UI updates - new user menu, updated about and profile screen and more (#723)

This commit is contained in:
Kevin Papst
2019-04-25 17:50:28 +02:00
committed by GitHub
parent 28801bf1ea
commit 56fe14c451
83 changed files with 1067 additions and 1089 deletions

View File

@@ -15,7 +15,7 @@ This is the _reloaded_ version of the open source timetracker Kimai.
It is built from scratch and doesn't share any source code with its [predecessor](http://www.kimai.org).
But it adapts the same ideas and a clean & simple UI for your time-tracking experience.
By now it is in an pre-stable development phase, usable and with most advanced features from Kimai 1.
By now it is in an almost-stable development phase, usable and with most advanced features from Kimai 1.
You can even [import your data](https://www.kimai.org/documentation/migration-v1.html) and start testing and using it today.
Kimai is a [multi-language application](https://www.kimai.org/documentation/translations.html) and already translated to english, german, italian, french, spanish, russian, arabic, hungarian and portuguese.

View File

@@ -43,7 +43,17 @@ $(function() {
}
);
$(modalSelector).modal('toggle');
location.reload();
$.kimai.reloadDatatableWithToolbarFilter();
},
resetVisibility: function (modalSelector) {
$(modalSelector).find('form').each(
function() {
var cookieName = $(this).attr('name');
Cookies.remove(cookieName);
}
);
$(modalSelector).modal('toggle');
$.kimai.reloadDatatableWithToolbarFilter();
},
changeVisibility: function (column) {
var tbl = $('table.dataTable');

View File

@@ -84,7 +84,7 @@ $(function() {
$select.find('option').remove().end().find('optgroup').remove().end();
if ($emptyOption.length != 0) {
if ($emptyOption.length !== 0) {
$select.append('<option value="">' + $emptyOption.text() + '</option>');
}

View File

@@ -39,3 +39,4 @@ footer.main-footer {
@import 'navbar';
@import 'daterangepicker';
@import 'tooltip';
@import 'skins';

View File

@@ -9,6 +9,11 @@
padding: 15px 0;
}
.content-header {
border-top: 1px solid rgba(0, 0, 0, 0.1);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1) !important;
background-color: #fff !important;
padding: 15px 0 15px 10px;
h1 {
small {
display: none;
@@ -16,6 +21,22 @@
}
}
.content-header>.breadcrumb {
position: relative;
margin-top: 5px;
top: 0;
right: 0;
float: none;
background: #d2d6de;
padding-left: 10px;
}
@media (min-width: $screen-sm-min) {
.content-header>.breadcrumb {
right: 10px;
}
}
@media (min-width: $screen-sm-min) {
.content-header {
h1 {
@@ -38,4 +59,14 @@ td {
margin: 0;
}
}
}
/* ABOUT SCREEN */
.box-body .menu-icon {
width: 35px;
height: 35px;
border-radius: 50%;
text-align: center;
line-height: 35px;
margin-right: 15px;
}

View File

@@ -21,9 +21,38 @@
text-align: center;
}
.dropdown-menu {
box-shadow: 0 8px 17px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
max-width: 90vw;
width: unset !important;
}
.user-menu {
margin-right: 10px;
ul {
li {
a {
color: #444;
i {
padding-right: 10px;
}
}
hr {
margin: 0;
}
}
}
}
}
@media (max-width: $screen-sm-min) {
.navbar-custom-menu>.navbar-nav>li>.dropdown-menu {
right: 0;
}
.navbar-nav .user-menu {
margin-right: 0;
}
.navbar-nav .user-menu.open .dropdown-menu > li > a {
padding: 5px 25px 5px 25px
}
}
/*

26
assets/sass/skins.scss Normal file
View File

@@ -0,0 +1,26 @@
$skins: blue, blue, black, green, purple, red, yellow;
@each $skinColor in $skins {
@media (max-width: $screen-sm-min) {
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li.divider {
background-color: #ccc;
}
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a {
color: #444;
}
.skin-#{$skinColor} .main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li.divider {
background-color: #ccc;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a {
color: #444;
}
.skin-#{$skinColor}-light .main-header .navbar .dropdown-menu li a:hover {
background: #fff;
}
}
}

14
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6c37cabf2eb9324df6bc8603447c404d",
"content-hash": "ca352d4ae4a75f38e97069a8c6b63fb0",
"packages": [
{
"name": "beberlei/DoctrineExtensions",
@@ -2967,16 +2967,16 @@
},
{
"name": "kevinpapst/adminlte-bundle",
"version": "2.4.2",
"version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/kevinpapst/AdminLTEBundle.git",
"reference": "9fec4d79c24f478f577b69759019c5f0912082f1"
"reference": "bdeedba21e863dd0a28ad3f505d6372021697c8c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/9fec4d79c24f478f577b69759019c5f0912082f1",
"reference": "9fec4d79c24f478f577b69759019c5f0912082f1",
"url": "https://api.github.com/repos/kevinpapst/AdminLTEBundle/zipball/bdeedba21e863dd0a28ad3f505d6372021697c8c",
"reference": "bdeedba21e863dd0a28ad3f505d6372021697c8c",
"shasum": ""
},
"require": {
@@ -3024,7 +3024,7 @@
}
],
"description": "Admin theme bundle for Symfony 4 based on AdminLTE 2.4.8 with FOSUserBundle support",
"time": "2019-04-10T18:10:15+00:00"
"time": "2019-04-25T14:54:29+00:00"
},
{
"name": "kimai/kimai2-composer",
@@ -10628,7 +10628,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": "^7.1.3",
"php": "^7.2",
"ext-gd": "*",
"ext-intl": "*",
"ext-mbstring": "*",

View File

@@ -1,10 +1,7 @@
#
# Configuration file for the AdminLTE bundle
#
# For more information about the settings visit:
# https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/configurations.md
#
admin_lte:
options:
# default image for missing user avatar images
@@ -20,15 +17,9 @@ admin_lte:
# whether the logo collapses or not
mini_sidebar: true
# tabs in the control_sidebar (right screen) can be configured, see:
# https://github.com/kevinpapst/AdminLTEBundle/blob/master/Resources/docs/control_sidebar.md
control_sidebar:
settings:
icon: "fas fa-user"
controller: 'App\Controller\SidebarController::settingsAction'
home:
icon: "fas fa-question-circle"
template: sidebar/home.html.twig
theme:
widget:
use_footer: false
routes:
adminlte_profile: user_profile

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,6 +1,6 @@
{
"build/app.js": "./app.js?79e85ebc5135d84e6896",
"build/app.css": "./app.css?31913e293b00471b96a78048069f0da2",
"build/app.js": "./app.js?3ed515aea79a52cbbfee",
"build/app.css": "./app.css?cbfb66c264fd0b333955ff7c7e93f50a",
"build/images/blue@2x.png": "./images/blue@2x.png?2694acfd",
"build/images/blue.png": "./images/blue.png?96f8a905",
"build/fonts/fa-solid-900.woff2": "./fonts/fa-solid-900.woff2?e8a92a29",

View File

@@ -914,7 +914,7 @@ class KimaiImporterCommand extends Command
* @param array $records
* @param array $fixedRates
* @param array $rates
* @return array
* @return int
* @throws \Exception
*/
protected function importTimesheetRecords(SymfonyStyle $io, array $records, array $fixedRates, array $rates)

View File

@@ -18,10 +18,7 @@ use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Routing\Annotation\Route;
/**
* Controller used for executing system relevant tasks.
*
* @Route(path="/admin/about")
* @Security("is_granted('system_information')")
* @Route(path="/about")
*/
class AboutController extends AbstractController
{
@@ -39,11 +36,13 @@ class AboutController extends AbstractController
}
/**
* @Route(path="", name="about", methods={"GET"})
* @Route(path="/debug", name="about_debug", methods={"GET"})
*
* @Security("is_granted('system_information')")
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function indexAction()
public function debugAction()
{
return $this->getAboutView();
}
@@ -90,16 +89,17 @@ class AboutController extends AbstractController
],
'info' => $phpInfo,
'settings' => $settings,
'license' => $this->getLicense(),
],
$additional
));
}
/**
* @return string
* @Route(path="", name="about", methods={"GET"})
*
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function getLicense()
public function license()
{
$filename = $this->projectDirectory . '/LICENSE';
@@ -114,7 +114,31 @@ class AboutController extends AbstractController
'Check this instead: ' . Constants::GITHUB . 'blob/master/LICENSE';
}
return $license;
return $this->render('about/license.html.twig', [
'license' => $license
]);
}
/**
* @Route(path="/flush-cache", name="system_flush_cache", methods={"GET"})
*
* @Security("is_granted('system_actions')")
*/
public function rebuildContainer(KernelInterface $kernel)
{
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'cache:clear',
'--env' => $kernel->getEnvironment(),
'-n',
]);
$output = new BufferedOutput();
$application->run($input, $output);
return $this->getAboutView(['content_action' => $output->fetch()]);
}
/**
@@ -153,26 +177,4 @@ class AboutController extends AbstractController
return $phpinfo['phpinfo'];
}
/**
* @Route(path="/flush-cache", name="system_flush_cache", methods={"GET"})
*
* @Security("is_granted('system_actions')")
*/
public function rebuildContainer(KernelInterface $kernel)
{
$application = new Application($kernel);
$application->setAutoExit(false);
$input = new ArrayInput([
'command' => 'cache:clear',
'--env' => $kernel->getEnvironment(),
'-n',
]);
$output = new BufferedOutput();
$application->run($input, $output);
return $this->getAboutView(['content_action' => $output->fetch()]);
}
}

View File

@@ -17,7 +17,6 @@ use App\Repository\ActivityRepository;
use App\Repository\Query\ActivityQuery;
use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

View File

@@ -15,7 +15,6 @@ use App\Entity\Timesheet;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

View File

@@ -18,7 +18,6 @@ use App\Form\UserPasswordType;
use App\Form\UserPreferencesForm;
use App\Form\UserRolesType;
use App\Repository\TimesheetRepository;
use App\Timesheet\UserDateTimeFactory;
use App\Voter\UserVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
@@ -45,21 +44,14 @@ class ProfileController extends AbstractController
*/
protected $encoder;
/**
* @var UserDateTimeFactory
*/
protected $dateTime;
/**
* @param UserPasswordEncoderInterface $encoder
* @param EventDispatcherInterface $dispatcher
* @param UserDateTimeFactory $dateTime
*/
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher, UserDateTimeFactory $dateTime)
public function __construct(UserPasswordEncoderInterface $encoder, EventDispatcherInterface $dispatcher)
{
$this->encoder = $encoder;
$this->dispatcher = $dispatcher;
$this->dateTime = $dateTime;
}
/**
@@ -77,7 +69,7 @@ class ProfileController extends AbstractController
*/
public function indexAction(User $profile)
{
return $this->getProfileView($profile, 'charts');
return $this->renderProfileView($profile, 'charts', 'user/stats.html.twig', []);
}
/**
@@ -149,7 +141,7 @@ class ProfileController extends AbstractController
return $this->redirectToRoute('user_profile_api_token', ['username' => $profile->getUsername()]);
}
return $this->getProfileView($profile, 'api-token', null, null, null, null, $form);
return $this->getProfileView($profile, 'api-token', null, null, null, $form);
}
/**
@@ -178,7 +170,7 @@ class ProfileController extends AbstractController
* @Route(path="/{username}/prefs", name="user_profile_preferences", methods={"GET", "POST"})
* @Security("is_granted('preferences', profile)")
*/
public function savePreferencesAction(User $profile, Request $request)
public function preferencesAction(User $profile, Request $request)
{
// we need to prepare the user preferences, which is done via an EventSubscriber
$event = new PrepareUserEvent($profile);
@@ -228,7 +220,11 @@ class ProfileController extends AbstractController
]);
}
return $this->getProfileView($profile, 'preferences', null, null, null, $form);
return $this->render('user/form.html.twig', [
'tab' => 'preferences',
'user' => $profile,
'form' => $form->createView(),
]);
}
/**
@@ -237,7 +233,6 @@ class ProfileController extends AbstractController
* @param Form|null $editForm
* @param Form|null $pwdForm
* @param Form|null $rolesForm
* @param Form|null $prefsForm
* @param Form|null $apiTokenForm
* @return \Symfony\Component\HttpFoundation\Response
* @throws \Doctrine\ORM\NonUniqueResultException
@@ -248,9 +243,36 @@ class ProfileController extends AbstractController
Form $editForm = null,
Form $pwdForm = null,
Form $rolesForm = null,
Form $prefsForm = null,
Form $apiTokenForm = null
) {
$forms = [];
if ($this->isGranted(UserVoter::EDIT, $user)) {
$editForm = $editForm ?: $this->createEditForm($user);
$forms['settings'] = $editForm->createView();
}
if ($this->isGranted(UserVoter::PASSWORD, $user)) {
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
$forms['password'] = $pwdForm->createView();
}
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
$forms['api-token'] = $apiTokenForm->createView();
}
if ($this->isGranted(UserVoter::ROLES, $user)) {
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
$forms['roles'] = $rolesForm->createView();
}
return $this->render('user/profile.html.twig', [
'tab' => $tab,
'user' => $user,
'forms' => $forms
]);
}
protected function renderProfileView(User $user, string $tab, string $template, array $vars)
{
/* @var $timesheetRepo TimesheetRepository */
$timesheetRepo = $this->getDoctrine()->getRepository(Timesheet::class);
$userStats = $timesheetRepo->getUserStatistics($user);
@@ -261,32 +283,9 @@ class ProfileController extends AbstractController
'user' => $user,
'stats' => $userStats,
'years' => $monthlyStats,
'local_time' => $this->dateTime->createDateTime(),
'forms' => []
];
if ($this->isGranted(UserVoter::EDIT, $user)) {
$editForm = $editForm ?: $this->createEditForm($user);
$viewVars['forms']['settings'] = $editForm->createView();
}
if ($this->isGranted(UserVoter::PASSWORD, $user)) {
$pwdForm = $pwdForm ?: $this->createPasswordForm($user);
$viewVars['forms']['password'] = $pwdForm->createView();
}
if ($this->isGranted(UserVoter::API_TOKEN, $user)) {
$apiTokenForm = $apiTokenForm ?: $this->createApiTokenForm($user);
$viewVars['forms']['api-token'] = $apiTokenForm->createView();
}
if ($this->isGranted(UserVoter::ROLES, $user)) {
$rolesForm = $rolesForm ?: $this->createRolesForm($user);
$viewVars['forms']['roles'] = $rolesForm->createView();
}
if ($this->isGranted(UserVoter::PREFERENCES, $user)) {
$prefsForm = $prefsForm ?: $this->createPreferencesForm($user);
$viewVars['forms']['preferences'] = $prefsForm->createView();
}
return $this->render('user/profile.html.twig', $viewVars);
return $this->render($template, array_merge($viewVars, $vars));
}
/**

View File

@@ -18,7 +18,6 @@ use App\Repository\ProjectRepository;
use App\Repository\Query\ProjectQuery;
use Doctrine\ORM\ORMException;
use Pagerfanta\Pagerfanta;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

View File

@@ -1,33 +0,0 @@
<?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\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\HttpFoundation\Request;
/**
* Sidebar controller
*
* @Security("is_granted('ROLE_USER')")
*/
class SidebarController extends AbstractController
{
/**
* @param Request $originalRequest
* @return \Symfony\Component\HttpFoundation\Response
*/
public function settingsAction(Request $originalRequest)
{
return $this->render('sidebar/settings.html.twig', [
'user' => $this->getUser(),
'originalRequest' => $originalRequest,
]);
}
}

View File

@@ -93,6 +93,7 @@ class AppExtension extends Extension
{
$container->setParameter('kimai.theme', $config);
$container->setParameter('kimai.theme.select_type', $config['select_type']);
$container->setParameter('kimai.theme.show_about', $config['show_about']);
}
/**

View File

@@ -292,6 +292,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('select_type')
->defaultNull()
->end()
->booleanNode('show_about')
->defaultTrue()
->end()
->end()
;

View File

@@ -92,7 +92,7 @@ class Extensions extends AbstractExtension
'trash' => 'far fa-trash-alt',
'user' => 'fas fa-user',
'visibility' => 'far fa-eye',
'settings' => 'fas fa-wrench',
'settings' => 'fas fa-cog',
'export' => 'fas fa-file-export',
'pdf' => 'fas fa-file-pdf',
'csv' => 'fas fa-table',
@@ -103,6 +103,10 @@ class Extensions extends AbstractExtension
'audit' => 'fas fa-history',
'home' => 'fas fa-home',
'shop' => 'fas fa-shopping-cart',
'about' => 'fas fa-info-circle',
'debug' => 'far fa-file-alt',
'profile-stats' => 'far fa-chart-bar',
'profile' => 'fas fa-user-edit',
];
/**

View File

@@ -74,7 +74,7 @@ class UserVoter extends AbstractVoter
switch ($attribute) {
// special case for the UserController
case self::DELETE:
if (!$this->canDelete($subject, $user)) {
if ($subject->getId() === $user->getId()) {
return false;
}
@@ -83,10 +83,16 @@ class UserVoter extends AbstractVoter
// used in templates and ProfileController
case self::VIEW:
case self::EDIT:
case self::API_TOKEN:
case self::PASSWORD:
case self::ROLES:
case self::PREFERENCES:
// always allow the user to edit these own settings
if ($subject->getId() === $user->getId()) {
return true;
}
// no break on purpose
case self::API_TOKEN:
case self::ROLES:
case self::HOURLY_RATE:
$permission .= $attribute;
break;
@@ -108,14 +114,4 @@ class UserVoter extends AbstractVoter
return $this->hasRolePermission($user, $permission);
}
/**
* @param User $profile
* @param User $user
* @return bool
*/
protected function canDelete(User $profile, User $user)
{
return $profile->getId() !== $user->getId();
}
}

View File

@@ -0,0 +1,60 @@
{% extends 'base.html.twig' %}
{% import "macros/actions.html.twig" as actions %}
{% block page_title %}{{ 'about.title'|trans({}, 'about') }}{% endblock %}
{% block page_actions %}{{ actions.about('index') }}{% endblock %}
{% block main %}
<div class="row">
<div class="col-md-3">
<div class="box box-primary">
<div class="box-body">
<img src="{{ asset('apple-touch-icon.png') }}" class="profile-user-img img-responsive img-circle" alt="Kimai logo" style="border: 0;">
<h3 class="profile-username text-center">{{ constant('App\\Constants::SOFTWARE') }}</h3>
<p class="text-muted text-center">
{{ constant('App\\Constants::VERSION') }}
{{ constant('App\\Constants::STATUS') }}
({{ constant('App\\Constants::NAME') }})
</p>
<hr>
<ul class="nav nav-stacked">
<li>
<a href="{{ ''|docu_link }}" target="_blank">
<i class="menu-icon fas fa-question bg-navy"></i>
{{ 'help'|trans({}, 'about') }}
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::GITHUB') }}" target="_blank">
<i class="menu-icon fab fa-github bg-gray"></i>
{{ 'support'|trans({}, 'about') }}
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::HOMEPAGE') }}" target="_blank">
<i class="menu-icon fa fa-home bg-green"></i>
{{ 'website'|trans({}, 'about') }}
</a>
</li>
</ul>
<hr>
<p class="text-center">
Made with ♥ by <a href="https://www.kevinpapst.de/">Kevin Papst</a> and <a href="{{ constant('App\\Constants::GITHUB') }}graphs/contributors" target="_blank">great contributors</a>.
</p>
</div>
</div>
{% block about_box %}{% endblock %}
</div>
<div class="col-md-9">
{% block about %}{% endblock %}
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,39 @@
{% extends 'about/layout.html.twig' %}
{% block about %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ constant('App\\Constants::SOFTWARE') }} is published under{% endblock %}
{% block box_body %}
<pre>
{{ license }}
</pre>
{% endblock %}
{% block box_footer %}
<a class="btn btn-default" href="https://choosealicense.com/licenses/mit/" target="_blank">choosealicense.com</a>
<a class="btn btn-default" href="https://en.wikipedia.org/wiki/MIT_License" target="_blank">wikipedia.org</a>
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}Special thanks ...{% endblock %}
{% block box_body %}
<p>... to the authors of the following libraries, Kimai wouldn't be possible without them:</p>
<ul>
<li>AdminLTE: <a href="https://adminlte.io" target="_blank">https://adminlte.io</a></li>
<li>Bootstrap: <a href="https://getbootstrap.com" target="_blank">https://getbootstrap.com</a></li>
<li>Bootstrap-Select: <a href="https://developer.snapappointments.com/bootstrap-select" target="_blank">https://developer.snapappointments.com/bootstrap-select</a></li>
<li>chart.js: <a href="https://www.chartjs.org" target="_blank">https://www.chartjs.org</a></li>
<li>DateRangePicker: <a href="http://www.daterangepicker.com/" target="_blank">http://www.daterangepicker.com/</a></li>
<li>Doctrine: <a href="https://www.doctrine-project.org" target="_blank">https://www.doctrine-project.org</a></li>
<li>FontAwesome: <a href="https://fontawesome.com" target="_blank">https://fontawesome.com</a></li>
<li>FullCalendar: <a href="https://fullcalendar.io" target="_blank">https://fullcalendar.io</a></li>
<li>jQuery: <a href="https://jquery.com" target="_blank">https://jquery.com</a></li>
<li>jQuery UI: <a href="https://jqueryui.com" target="_blank">http://jqueryui.com</a></li>
<li>MomentJS: <a href="https://momentjs.com" target="_blank">https://momentjs.com</a></li>
<li>PHPOffice: <a href="https://github.com/PHPOffice" target="_blank">https://github.com/PHPOffice</a></li>
<li>Symfony: <a href="https://symfony.com" target="_blank">https://symfony.com</a></li>
<li>...</li>
</ul>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -1,149 +1,69 @@
{% extends 'base.html.twig' %}
{% extends 'about/layout.html.twig' %}
{% import "macros/widgets.html.twig" as widgets %}
{% block page_title %}{{ 'about.title'|trans({}, 'about') }}{% endblock %}
{% block page_subtitle %}{{ 'about.subtitle'|trans({}, 'about') }}{% endblock %}
{% block main %}
{% block about_box %}
{% if is_granted('system_actions') %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' with {'boxtype': 'warning'} %}
{% block box_title %}{{ 'label.actions'|trans }}{% endblock %}
{% block box_body %}
{% if content_action is defined and content_action is not empty %}
<pre>
{{ content_action }}
</pre>
{% endif %}
{{ widgets.callout('info', 'Thank you for using Kimai ' ~ constant('App\\Constants::VERSION') ~ ' ' ~ constant('App\\Constants::STATUS') ~ ' - ' ~ constant('App\\Constants::NAME'), 'Kimai ' ~ constant('App\\Constants::VERSION'), 'fas fa-thumbs-up') }}
<div class="row">
<div class="col-md-12">
<div class="nav-tabs-custom">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation"class ="active"><a href="#systeminfo" aria-controls="systeminfo" role="tab" data-toggle="tab">{{ 'tab.system'|trans({}, 'about') }}</a></li>
<li role="presentation"><a href="#license" aria-controls="license" role="tab" data-toggle="tab">{{ 'tab.license'|trans({}, 'about') }}</a></li>
<li role="presentation"><a href="#thanks" aria-controls="thanks" role="tab" data-toggle="tab">{{ 'tab.thanks'|trans({}, 'about') }}</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="systeminfo">
<div class="box-group" id="accordion">
{% if is_granted('system_actions') %}
<div class="panel box">
<div class="box-header with-border">
<h4 class="box-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseSystemAction" aria-expanded="true">
{{ 'label.actions'|trans }}
</a>
</h4>
</div>
<div id="collapseSystemAction" class="panel-collapse collapse in" aria-expanded="true">
<div class="box-body">
<p>
<a href="{{ path('system_flush_cache') }}" class="btn btn-primary" onclick="$(this).html('Clear cache <i class=\'fas fa-spinner fa-spin\'></i>')">Clear cache</a>
</p>
{% if content_action is defined and content_action is not empty %}
<pre>
{{ content_action }}
</pre>
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="panel box">
<div class="box-header with-border">
<h4 class="box-title">
<a data-toggle="collapse" role="button" data-parent="#accordion" href="#collapseEnvironment" aria-expanded="false" aria-controls="collapseEnvironment">
Environment
</a>
</h4>
</div>
<div id="collapseEnvironment" class="panel-collapse collapse" aria-expanded="false" role="tabpanel">
<div class="box-body">
<dl>
{% for name, value in dotenv %}
<dt>{{ name }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
</div>
</div>
</div>
<div class="panel box">
<div class="box-header with-border">
<h4 class="box-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapsePhp" class="collapsed" aria-expanded="false">
PHP
</a>
</h4>
</div>
<div id="collapsePhp" class="panel-collapse collapse" aria-expanded="false">
<div class="box-body">
<dl>
<dt>Version</dt><dd>{{ constant('PHP_VERSION') }}</dd>
<dt>Modules</dt><dd>{{ modules|join(', ') }}</dd>
{% for name, value in settings %}
<dt>{{ name }}</dt>
<dd>
{% if value is empty %}
<i>unknown</i>
{% else %}
{{ value }}
{% endif %}
</dd>
{% endfor %}
</dl>
</div>
</div>
</div>
<div class="panel box">
<div class="box-header with-border">
<h4 class="box-title">
<a data-toggle="collapse" data-parent="#accordion" href="#collapseSystem" class="collapsed" aria-expanded="false">
Server
</a>
</h4>
</div>
<div id="collapseSystem" class="panel-collapse collapse" aria-expanded="false">
<div class="box-body">
<dl>
{% for name, value in info %}
<dt>{{ name }}</dt>
<dd>{{ value|raw }}</dd>
{% endfor %}
</dl>
</div>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="license">
<pre>{{ license }}</pre>
<p>
<a href="https://choosealicense.com/licenses/mit/" target="_blank">choosealicense.com</a>,
<a href="https://en.wikipedia.org/wiki/MIT_License" target="_blank">wikipedia.org</a>
</p>
</div>
<div role="tabpanel" class="tab-pane" id="thanks">
<p>Special thanks to the authors of the following libraries, Kimai wouldn't be possible without them:</p>
<ul>
<li>AdminLTE: <a href="https://adminlte.io" target="_blank">https://adminlte.io</a></li>
<li>Bootstrap: <a href="https://getbootstrap.com" target="_blank">https://getbootstrap.com</a></li>
<li>Bootstrap-Select: <a href="https://developer.snapappointments.com/bootstrap-select" target="_blank">https://developer.snapappointments.com/bootstrap-select</a></li>
<li>chart.js: <a href="https://www.chartjs.org" target="_blank">https://www.chartjs.org</a></li>
<li>DateRangePicker: <a href="http://www.daterangepicker.com/" target="_blank">http://www.daterangepicker.com/</a></li>
<li>Doctrine: <a href="https://www.doctrine-project.org" target="_blank">https://www.doctrine-project.org</a></li>
<li>FontAwesome: <a href="https://fontawesome.com" target="_blank">https://fontawesome.com</a></li>
<li>FullCalendar: <a href="https://fullcalendar.io" target="_blank">https://fullcalendar.io</a></li>
<li>jQuery: <a href="https://jquery.com" target="_blank">https://jquery.com</a></li>
<li>jQuery UI: <a href="https://jqueryui.com" target="_blank">http://jqueryui.com</a></li>
<li>MomentJS: <a href="https://momentjs.com" target="_blank">https://momentjs.com</a></li>
<li>Symfony: <a href="https://symfony.com" target="_blank">https://symfony.com</a></li>
<li>...</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<p>
<a href="{{ path('system_flush_cache') }}" class="btn btn-primary" onclick="$(this).html('Clear cache <i class=\'fas fa-spinner fa-spin\'></i>')">Clear cache</a>
</p>
{% endblock %}
{% endembed %}
{% endif %}
{% endblock %}
{% block about %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}Environment{% endblock %}
{% block box_body %}
<dl>
{% for name, value in dotenv %}
<dt>{{ name }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}PHP{% endblock %}
{% block box_body %}
<dl>
<dt>Version</dt><dd>{{ constant('PHP_VERSION') }}</dd>
<dt>Modules</dt><dd>{{ modules|join(', ') }}</dd>
{% for name, value in settings %}
<dt>{{ name }}</dt>
<dd>
{% if value is empty %}
<i>unknown</i>
{% else %}
{{ value }}
{% endif %}
</dd>
{% endfor %}
</dl>
{% endblock %}
{% endembed %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}Server{% endblock %}
{% block box_body %}
<dl>
{% for name, value in info %}
<dt>{{ name }}</dt>
<dd>{{ value|raw }}</dd>
{% endfor %}
</dl>
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -20,9 +20,9 @@
{% set columns = {
'name': 'alwaysVisible',
'customer': 'hidden-xs',
'project': '',
'project': 'hidden-xs',
'comment': 'hidden-xs',
'visible': 'hidden-xs',
'visible': '',
'actions': 'alwaysVisible',
} %}

View File

@@ -1,6 +1,6 @@
{% extends '@AdminLTE/layout/default-layout-avanzu.html.twig' %}
{% extends '@AdminLTE/layout/default-layout.html.twig' %}
{% block avanzu_after_body_start %}
{% block after_body_start %}
{% embed 'embeds/modal.html.twig' %}
{% block modal_id %}remote_form_modal{% endblock %}
{% block modal_title %}{% endblock %}
@@ -9,7 +9,7 @@
{% endembed %}
{% endblock %}
{% block avanzu_page_content_before %}
{% block page_content_before %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_BEFORE')) %}
{{ event.content|raw }}
<div class="toolbar-pad no-print">
@@ -17,13 +17,13 @@
</div>
{% endblock %}
{% block avanzu_page_content_after %}
{% block page_content_after %}
{% block main_after %}{% endblock %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_AFTER')) %}
{{ event.content|raw }}
{% endblock %}
{% block avanzu_page_content %}
{% block page_content %}
{% set event = trigger(constant('App\\Event\\ThemeEvent::CONTENT_START')) %}
{{ event.content|raw }}
{% block main %}{% endblock %}
@@ -31,33 +31,25 @@
{{ event.content|raw }}
{% endblock %}
{% block avanzu_document_title %}
{% block title %}{{ 'browser.title'|trans }}{% endblock %}
{% block title %}
{{ 'browser.title'|trans }}
{% endblock %}
{% block avanzu_page_title %}
{% block page_title %}{% endblock %}
{% endblock %}
{% block page_subtitle %}{% endblock %}
{% block avanzu_page_subtitle %}
{% block page_subtitle %}{% endblock %}
{% endblock %}
{% block avanzu_logo_mini %}
{% block logo_mini %}
<b>K</b>TT
{% endblock %}
{% block avanzu_logo_lg %}
{% block logo_large %}
<b>Kimai</b> - Time Tracking
{% endblock %}
{% block avanzu_footer %}
{% block footer %}
{% block footer %}
<!-- Page rendered on {{ 'now'|localizeddate('long', 'long', null, 'UTC') }} -->
{% endblock %}
{% endblock %}
{% block avanzu_navbar_messages %}
{% block navbar_messages %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{% if is_granted('view_own_timesheet') %}
<li class="visible-xs-inline-block">
@@ -85,23 +77,76 @@
{% endif %}
{% endblock %}
{% block avanzu_navbar_notifications %}
{% block navbar_notifications %}
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
{{ render(controller('App\\Controller\\NavbarController::recentActivitiesAction')) }}
{% endif %}
{% endblock %}
{# deactivated blocks #}
{% block avanzu_navbar_tasks %}{% endblock %}
{% block avanzu_navbar_user %}{% endblock %}
{% block avanzu_sidebar_user %}{% endblock %}
{% block avanzu_sidebar_search %}{% endblock %}
{% block navbar_tasks %}{% endblock %}
{% block sidebar_user %}{% endblock %}
{% block sidebar_search %}{% endblock %}
{% block avanzu_breadcrumb %}
{{ block('page_actions') }}
{% block navbar_user %}
{% import "macros/widgets.html.twig" as widgets %}
<li class="dropdown user-menu">
<a href="#" class="dropdown-toggle ddt-large" data-toggle="dropdown">
<i class="{{ 'user'|icon }} fa-2x"></i>
</a>
<ul class="dropdown-menu">
{% if app.user is not null %}
<li>
<a href="{{ path('user_profile', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'profile-stats'|icon }}"></i>
{{ widgets.username(app.user) }}
</h4>
</a>
</li>
<li>
<a href="{{ path('user_profile_edit', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'profile'|icon }}"></i>
{{ 'profile.title'|trans }}
</h4>
</a>
</li>
<li>
<a href="{{ path('user_profile_preferences', {'username' : app.user.username}) }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'settings'|icon }}"></i>
{{ 'profile.preferences'|trans }}
</h4>
</a>
</li>
<li>
<a href="{{ path('fos_user_security_logout') }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'logout'|icon }}"></i>
{{ 'menu.logout'|trans }}
</h4>
</a>
</li>
{% endif %}
{% if kimai_context.show_about %}
<li class="divider"></li>
<li>
<a href="{{ path('about') }}">
<h4 class="control-sidebar-subheading">
<i class="{{ 'about'|icon }}"></i>
{{ 'about.title'|trans({}, 'about') }}
</h4>
</a>
</li>
{% endif %}
</ul>
</li>
{% endblock %}
{% block page_actions %}{% endblock %}
{% block breadcrumb %}
{% block page_actions %}{% endblock %}
{% endblock %}
{% block stylesheets %}
{# we do not call parent() as we use a custom built for the frontend assets and don't want the default <stylesheet> #}

View File

@@ -1,31 +1,5 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'http_error.title'|trans({}, 'exceptions') }}{% endblock %}
{% block page_subtitle %}{{ status_code }}{% endblock %}
{% extends 'error.html.twig' %}
{% block main %}
{% set errorTitleKey = "http_error_#{status_code}.description" %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorTitleKey == errorTitleTrans %}
{% set errorTitleKey = 'http_error.description' %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% set errorMessageKey = "http_error_#{status_code}.suggestion" %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorMessageKey == errorMessageTrans %}
{% set errorMessageKey = 'http_error.suggestion' %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
<div class="error-page">
<h2 class="headline text-yellow"> {{ status_code }}</h2>
<div class="error-content">
<h3><i class="fas fa-unlock-alt text-yellow"></i> {{ errorTitleTrans }}</h3>
<p>{{ errorMessageTrans }}</p>
</div>
</div>
{% block error_icon %}
<i class="fas fa-unlock-alt text-yellow"></i>
{% endblock %}
{% block avanzu_control_sidebar %}{% endblock %}
{% block avanzu_navbar_control_sidebar_toggle %}{% endblock %}

View File

@@ -1,31 +1 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'http_error.title'|trans({}, 'exceptions') }}{% endblock %}
{% block page_subtitle %}{{ status_code }}{% endblock %}
{% block main %}
{% set errorTitleKey = "http_error_#{status_code}.description" %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorTitleKey == errorTitleTrans %}
{% set errorTitleKey = 'http_error.description' %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% set errorMessageKey = "http_error_#{status_code}.suggestion" %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorMessageKey == errorMessageTrans %}
{% set errorMessageKey = 'http_error.suggestion' %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
<div class="error-page">
<h2 class="headline text-yellow"> {{ status_code }}</h2>
<div class="error-content">
<h3><i class="fas fa-unlock-alt text-yellow"></i> {{ errorTitleTrans }}</h3>
<p>{{ errorMessageTrans }}</p>
</div>
</div>
{% endblock %}
{% block avanzu_control_sidebar %}{% endblock %}
{% block avanzu_navbar_control_sidebar_toggle %}{% endblock %}
{% extends 'error.html.twig' %}

View File

@@ -17,8 +17,8 @@
{% endif %}
{% endblock %}
{% block modal_footer %}
<button type="submit" class="btn {% if used %}btn-outline{% else %}btn-primary{% endif %} pull-left modal-form-save" data-loading-text="{{ 'action.delete'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.delete'|trans }}</button>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="submit" class="btn {% if used %}btn-outline{% else %}btn-primary{% endif %} modal-form-save" data-loading-text="{{ 'action.delete'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.delete'|trans }}</button>
{% endblock %}
{% block modal_end %}{{ form_end(form) }}{% endblock %}
{% endembed %}

View File

@@ -11,8 +11,8 @@
{{ form_widget(form) }}
{% endblock %}
{% block modal_footer %}
<button type="submit" class="btn btn-primary pull-left modal-form-save" data-loading-text="{{ 'action.save'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.save'|trans }}</button>
<button type="button" class="btn btn-default btn-cancel" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="submit" class="btn btn-primary modal-form-save" data-loading-text="{{ 'action.save'|trans }}..." id="{{ block('modal_id') }}_save">{{ 'action.save'|trans }}</button>
{% endblock %}
{% block modal_end %}{{ form_end(form) }}{% endblock %}
{% endembed %}

33
templates/error.html.twig Normal file
View File

@@ -0,0 +1,33 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'http_error.title'|trans({}, 'exceptions') }}{% endblock %}
{% block page_subtitle %}{{ status_code }}{% endblock %}
{% block main %}
{% set errorTitleKey = "http_error_#{status_code}.description" %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorTitleKey == errorTitleTrans %}
{% set errorTitleKey = 'http_error.description' %}
{% set errorTitleTrans = errorTitleKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
{% set errorMessageKey = "http_error_#{status_code}.suggestion" %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% if errorMessageKey == errorMessageTrans %}
{% set errorMessageKey = 'http_error.suggestion' %}
{% set errorMessageTrans = errorMessageKey|trans({ '%status_code%': status_code }, 'exceptions') %}
{% endif %}
<div class="error-page">
<h2 class="headline text-yellow"> {{ status_code }}</h2>
{% block error_content %}
<div class="error-content">
<h3>{% block error_icon %}{% endblock %} {{ errorTitleTrans }}</h3>
<p>{{ errorMessageTrans }}</p>
<a class="btn btn-primary" href="javascript:window.history.back();">{{ 'action.back'|trans }}</a>
</div>
{% endblock %}
</div>
{% endblock %}

View File

@@ -8,6 +8,7 @@
{% block page_actions %}
{% set actions = {
'filter': '#collapseExport',
'visibility': '#modal_export',
'off': {'id':'export-toggle-button'}
} %}
{{ widgets.page_actions(actions) }}
@@ -27,13 +28,13 @@
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% else %}
{% set columns = {
'date': '',
'date': 'alwaysVisible',
'user': 'hidden-xs hidden-sm',
'customer': 'hidden-xs hidden-sm',
'project': 'hidden-xs hidden-sm',
'activity': 'hidden-xs hidden-sm',
'description': 'hidden-xs hidden-sm',
'unit_price': '',
'unit_price': 'hidden-xs',
'duration': '',
'total_rate': '',
'exported': 'alwaysVisible',
@@ -55,20 +56,20 @@
{% endif %}
<tr>
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
<td class="hidden-xs hidden-sm">{{ widgets.label_user(entry.user) }}</td>
<td class="hidden-xs hidden-sm">{{ widgets.label_customer(entry.project.customer) }}</td>
<td class="hidden-xs hidden-sm">{{ widgets.label_project(entry.project) }}</td>
<td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td>
<td class="hidden-xs hidden-sm timesheet-description">
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer') }}">{{ widgets.label_customer(entry.project.customer) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">{{ widgets.label_project(entry.project) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'activity') }}">{{ widgets.label_activity(entry.activity) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
{{ entry.description }}
</td>
<td class="text-nowrap">
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-nowrap">
{{ rate|money(currency) }}
</td>
<td class="text-nowrap">
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }} text-nowrap">
{{ duration }}
</td>
<td class="text-nowrap">
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }} text-nowrap">
{{ entry.rate|money(currency) }}
</td>
<td>

View File

@@ -6,7 +6,7 @@
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
{% block page_actions %}
{% set actions = {'filter': '#collapseInvoice'} %}
{% set actions = {'filter': '#collapseInvoice', 'visibility': '#modal_invoice'} %}
{% if is_granted('create_invoice_template') %}
{% set actions = actions|merge({'create': path('admin_invoice_template_create')}) %}
{% endif %}
@@ -37,10 +37,10 @@
{{ widgets.callout('success', 'invoice.preview') }}
{% set columns = {
'date': '',
'user': 'hidden-xs hidden-sm',
'activity': 'hidden-xs hidden-sm',
'unit_price': 'text-center',
'date': 'alwaysVisible',
'user': 'hidden-sm',
'description': 'hidden-xs hidden-sm',
'unit_price': 'hidden-xs text-center',
'amount': 'text-center',
'total_rate': 'text-right',
} %}
@@ -60,17 +60,17 @@
{% endif %}
<tr>
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
<td class="hidden-xs hidden-sm">{{ widgets.label_user(entry.user) }}</td>
<td class="hidden-xs hidden-sm timesheet-description">
<td class="{{ tables.data_table_column_class(tableName, columns, 'user') }}">{{ widgets.label_user(entry.user) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'description') }} timesheet-description">
{% if entry.description is not empty %}
{{ entry.description|desc2html }}
{% else %}
{{ entry.activity.name }} / {{ entry.project.name }}
{% endif %}
</td>
<td class="text-center">{{ rate|money(model.calculator.currency) }}</td>
<td class="text-center text-nowrap">{{ duration }}</td>
<td class="text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'unit_price') }} text-center">{{ rate|money(model.calculator.currency) }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'amount') }} text-center text-nowrap">{{ duration }}</td>
<td class="{{ tables.data_table_column_class(tableName, columns, 'total_rate') }} text-right text-nowrap">{{ entry.rate|money(model.calculator.currency) }}</td>
</tr>
{% endfor %}
{{ tables.data_table_footer(entries) }}

View File

@@ -56,7 +56,7 @@
<thead>
<tr>
<th>{{ 'label.date'|trans }}</th>
<th>{{ 'label.activity'|trans }}</th>
<th>{{ 'label.description'|trans }}</th>
<th>{{ 'label.unit_price'|trans }}</th>
<th>{{ 'label.hours'|trans }}</th>
<th>{{ 'label.total_rate'|trans }}</th>

View File

@@ -244,3 +244,31 @@
{{ widgets.page_actions(event.payload.actions) }}
{% endif %}
{% endmacro %}
{% macro about(view) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {'about': {'url': path('about')}} %}
{% if is_granted('system_information') %}
{% set actions = actions|merge({'debug': path('about_debug')}) %}
{% endif %}
{% set event = trigger('actions.about', {'actions': actions, 'view': view}) %}
{{ widgets.page_actions(event.payload.actions) }}
{% endmacro %}
{% macro profile(view, user) %}
{% import "macros/widgets.html.twig" as widgets %}
{% set actions = {'profile-stats': {'url': path('user_profile', {'username' : user.username})}} %}
{% if is_granted('edit', user) %}
{% set actions = actions|merge({'profile': {'url': path('user_profile_edit', {'username' : user.username})}}) %}
{% endif %}
{% if is_granted('preferences', user) %}
{% set actions = actions|merge({'settings': {'url': path('user_profile_preferences', {'username' : user.username})}}) %}
{% endif %}
{% set event = trigger('actions.profile', {'actions': actions, 'view': view}) %}
{{ widgets.page_actions(event.payload.actions) }}
{% endmacro %}

View File

@@ -21,8 +21,9 @@
<p>{{ 'modal.columns.description'|trans }}</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary pull-left" onclick="$.datatable.saveVisibility('#modal_{{ name }}')">{{ 'action.save'|trans }}</button>
<button type="button" class="btn btn-warning pull-left" onclick="$.datatable.resetVisibility('#modal_{{ name }}')">{{ 'action.delete'|trans }}</button>
<button type="button" class="btn btn-default" data-dismiss="modal">{{ 'action.close'|trans }}</button>
<button type="button" class="btn btn-primary" onclick="$.datatable.saveVisibility('#modal_{{ name }}')">{{ 'action.save'|trans }}</button>
</div>
</div>
</div>

View File

@@ -19,10 +19,10 @@
{% else %}
{% set columns = {
'name': 'alwaysVisible',
'customer': '',
'customer': 'hidden-xs',
'comment': 'hidden-xs hidden-sm',
'budget': 'hidden-xs',
'visible': 'hidden-xs',
'visible': '',
'actions': 'alwaysVisible',
} %}

View File

@@ -1,45 +0,0 @@
{% import "macros/widgets.html.twig" as widgets %}
{% set version = constant('App\\Constants::VERSION') ~ ' ' ~ constant('App\\Constants::STATUS') %}
<h3 class="control-sidebar-heading">Kimai v2 - {{ version }}</h3>
<div class="form-group">
{{ 'home.betainfo'|trans({'%version%': version}, 'sidebar')|raw|nl2br }}
</div>
<ul class="control-sidebar-menu">
<li>
<a href="{{ constant('App\\Constants::GITHUB') }}" target="_blank">
<i class="menu-icon fab fa-github bg-gray"></i>
<div class="menu-info">
<h4 class="control-sidebar-subheading">GitHub</h4>
<p>{{ 'home.github'|trans({}, 'sidebar')|raw }}</p>
</div>
</a>
</li>
<li>
<a href="{{ constant('App\\Constants::HOMEPAGE') }}" target="_blank">
<i class="menu-icon fa fa-home bg-green"></i>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'home.website'|trans({}, 'sidebar') }}</h4>
<p>www.kimai.org</p>
</div>
</a>
</li>
{% if is_granted('system_information') %}
<li>
<a href="{{ path('about') }}">
<i class="menu-icon far fa-copyright bg-aqua"></i>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'about.title'|trans({}, 'about') }}</h4>
<p>{{ 'about.subtitle'|trans({}, 'about') }}</p>
</div>
</a>
</li>
{% endif %}
</ul>
<p><br>Made with ♥ by <a href="https://www.kevinpapst.de/">Kevin Papst</a> and <a href="{{ constant('App\\Constants::GITHUB') }}graphs/contributors" target="_blank">great contributors</a>.</p>

View File

@@ -1,68 +0,0 @@
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
{% set linkUserProfile = path('user_profile', {'username' : user.username}) %}
{% if not is_granted('view_own_profile') %}
{% set linkUserProfile = '#' %}
{% endif %}
<ul class="control-sidebar-menu">
<li>
<a href="{{ linkUserProfile }}">
<div class="pull-left image">
{{ macro.avatar(user.avatar, user.username, 'img-circle') }}
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ widgets.username(user) }}</h4>
<p>{{ user.title }}</p>
</div>
</a>
</li>
</ul>
<ul class="control-sidebar-menu">
{% if is_granted('edit_own_profile') %}
<li>
<a href="{{ path('user_profile_edit', {'username' : user.username}) }}">
<div class="pull-left image">
<i class="{{ 'user'|icon }} text-gray"></i>
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'profile.title'|trans }}</h4>
</div>
</a>
</li>
{% endif %}
{% if is_granted('preferences_own_profile') %}
<li>
<a href="{{ path('user_profile_preferences', {'username' : user.username}) }}">
<div class="pull-left image">
<i class="{{ 'settings'|icon }} text-gray"></i>
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'profile.preferences'|trans }}</h4>
</div>
</a>
</li>
{% endif %}
<li>
<a href="{{ ''|docu_link }}" target="_blank">
<div class="pull-left image">
<i class="{{ 'help'|icon }} text-gray"></i>
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'home.help'|trans({}, 'sidebar') }}</h4>
</div>
</a>
</li>
<li>
<a href="{{ path('fos_user_security_logout') }}">
<div class="pull-left image">
<i class="{{ 'logout'|icon }} text-gray"></i>
</div>
<div class="menu-info">
<h4 class="control-sidebar-subheading">{{ 'menu.logout'|trans }}</h4>
</div>
</a>
</li>
</ul>

View File

@@ -0,0 +1,16 @@
{% extends 'user/layout.html.twig' %}
{% import "macros/actions.html.twig" as actions %}
{% block main %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ ('profile.' ~ tab)|trans }}{% endblock %}
{% block box_body %}
{{ form_start(form) }}
{{ form_widget(form) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form) }}
{% endblock %}
{% endembed %}
{% endblock %}

View File

@@ -28,7 +28,7 @@
'username': 'hidden-xs',
'email': 'hidden-xs hidden-sm',
'title': 'hidden-xs',
'roles': '',
'roles': 'hidden-xs',
'active': '',
'actions': 'alwaysVisible',
} %}

View File

@@ -0,0 +1,151 @@
{% extends 'base.html.twig' %}
{% import "macros/actions.html.twig" as actions %}
{% block page_title %}{{ 'profile.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'profile.subtitle'|trans }}{% endblock %}
{% block page_actions %}{{ actions.profile(tab, user) }}{% endblock %}
{% block main %}
{% import _self as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="row">
<div class="col-md-9">
{% block profile_content %}{% endblock %}
</div>
<div class="col-md-3">
{{ macro.profile_box(user, stats) }}
{{ macro.profile_infos(user, stats) }}
</div>
</div>
{% endblock %}
{% macro profile_infos(user, stats) %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'profile.about_me'|trans }}</h3>
</div>
<div class="box-body">
<ul class="nav nav-stacked">
{# colors = purple, blue, aqua, red, green #}
<li><a href="#">{{ 'label.id'|trans }} <span class="pull-right badge bg-aqua">{{ user.id }}</span></a></li>
<li><a href="#">{{ 'label.username'|trans }} <span class="pull-right badge bg-blue">{{ user.username }}</span></a></li>
<li><a href="#">{{ 'profile.first_entry'|trans }} <span class="pull-right badge bg-purple">{{ stats.firstEntry|date_short }}</span></a></li>
{% if is_granted('view_rate_own_timesheet') %}
<li><a href="#">{{ 'label.hourlyRate'|trans }} <span class="pull-right badge bg-blue">{{ user.preferenceValue('hourly_rate') }}</span></a></li>
{% endif %}
</ul>
</div>
</div>
{% endmacro %}
{% macro profile_box(user, stats) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-primary">
<div class="box-body box-profile">
{{ macro.avatar(user.avatar, user.username, 'profile-user-img img-responsive img-circle') }}
<h3 class="profile-username text-center">{{ widgets.username(user) }}</h3>
<p class="text-muted text-center">{{ user.title }}</p>
<ul class="list-group list-group-unbordered">
<li class="list-group-item">
<b>{{ 'stats.durationMonth'|trans }}</b> <a class="pull-right">{{ stats.durationThisMonth|duration }}</a>
</li>
{% if is_granted('view_rate_own_timesheet') %}
<li class="list-group-item">
<b>{{ 'stats.amountMonth'|trans }}</b> <a class="pull-right">{{ stats.amountThisMonth|money }}</a>
</li>
{% endif %}
<li class="list-group-item">
<b>{{ 'stats.durationTotal'|trans }}</b> <a class="pull-right">{{ stats.durationTotal|duration }}</a>
</li>
{% if is_granted('view_rate_own_timesheet') %}
<li class="list-group-item">
<b>{{ 'stats.amountTotal'|trans }}</b> <a class="pull-right">{{ stats.amountTotal|money }}</a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_list_unused(user, items, color) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user-2">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<!-- /.widget-user-image -->
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="box-footer no-padding">
<ul class="nav nav-stacked">
{% for entry in items %}
<li><a href="{{ entry.url }}">{{ entry.title|trans }} <span class="pull-right badge bg-{{ entry.color }}">{{ entry.value }}</span></a></li>
{% endfor %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_box_unused(user, stats, color) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user">
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<div class="box-footer">
<div class="row">
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationTotal|duration }}</h5>
<span class="description-text">{{ 'stats.durationTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountTotal|money }}</h5>
<span class="description-text">{{ 'stats.amountTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationThisMonth|duration }}</h5>
<span class="description-text">{{ 'stats.durationMonth'|trans }}</span>
</div>
</div>
<div class="col-sm-6 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountThisMonth|money }}</h5>
<span class="description-text">{{ 'stats.amountMonth'|trans }}</span>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -1,117 +1,24 @@
{% extends 'base.html.twig' %}
{% block page_title %}{{ 'profile.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'profile.subtitle'|trans }}{% endblock %}
{% extends 'user/layout.html.twig' %}
{% import "macros/actions.html.twig" as actions %}
{% block main %}
{% import _self as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="row">
<div class="col-md-3">
{{ macro.profile_box(user, stats) }}
{{ macro.profile_infos(user, stats, local_time) }}
</div>
<div class="col-md-9">
<div class="col-md-12">
<div class="nav-tabs-custom">
<ul class="nav nav-tabs">
<li {% if tab == "charts" %}class="active"{% endif %}><a href="#charts" data-toggle="tab" aria-expanded="true">{{ 'profile.tab_monthly'|trans }}</a></li>
{% for formName, form in forms %}
<li {% if tab == formName %}class="active"{% endif %}><a href="#{{ formName }}" data-toggle="tab" aria-expanded="false">{{ ('profile.' ~ formName)|trans }}</a></li>
{% endfor %}
</ul>
<div class="tab-content">
<div class="tab-pane {% if tab == "charts" %}active{% endif %}" id="charts">
{% if years is empty %}
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %}
<script type="text/javascript">
var userProfileChartOptions = {
maintainAspectRatio : true,
responsive : true,
legend : false,
barPercentage : 0.5,
categoryPercentage : 0.9,
scales: {
xAxes: [{
gridLines: {
display: false
}
}],
yAxes: [{
gridLines: {
display: true,
color: 'rgba(0,0,0,.05)',
lineWidth: 1
}
}]
}
};
var userProfileChartLabels = [
{% for i in 1..12 %}
'{{ ('month.' ~i)|trans }}'
{% if not loop.last %},{% endif %}
{% endfor %}
];
</script>
{% for year,yearStat in years %}
<h2>{{ year }}</h2>
<div class="chart">
<canvas id="userProfileChart{{ year }}" style="height: 200px;"></canvas>
</div>
<script type="text/javascript">
$(document).ready(function () {
var userProfileChartData{{ year }} = {
labels: userProfileChartLabels,
datasets: [
{
label: '{{ year }}',
backgroundColor : '#00a65a',
borderColor : '#00a65a',
data: [
{% for month in yearStat.months %}
{{ (month.totalDuration / 3600)|round }}
{% if not loop.last %},{% endif %}
{% endfor %}
]
}
]
};
var userProfileChartCanvas{{ year }} = $("#userProfileChart{{ year }}").get(0).getContext("2d");
{% if tab == 'charts' %}
var userProfileChart{{ year }} = new Chart(
userProfileChartCanvas{{ year }}, {
type: 'bar',
data: userProfileChartData{{ year }},
options: userProfileChartOptions
}
);
{% endif %}
$('a[href="#charts"]').on('shown.bs.tab', function(event){
var userProfileChart{{ year }} = new Chart(
userProfileChartCanvas{{ year }}, {
type: 'bar',
data: userProfileChartData{{ year }},
options: userProfileChartOptions
}
);
});
});
</script>
{% endfor %}
</div>
{% for formName, form in forms %}
<div class="tab-pane {% if tab == formName %}active{% endif %}" id="{{ formName }}">
{{ form_start(form) }}
{{ form_widget(form) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_widget(form) }}
<input type="submit" value="{{ 'action.save'|trans }}" class="btn btn-primary" />
{{ form_end(form) }}
</div>
{% endfor %}
@@ -120,131 +27,3 @@
</div>
</div>
{% endblock %}
{% macro profile_infos(user, stats, local_time) %}
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">{{ 'profile.about_me'|trans }}</h3>
</div>
<div class="box-body">
<ul class="nav nav-stacked">
{# colors = purple, blue, aqua, red, green #}
<li><a href="#">{{ 'label.id'|trans }} <span class="pull-right badge bg-aqua">{{ user.id }}</span></a></li>
<li><a href="#">{{ 'label.username'|trans }} <span class="pull-right badge bg-blue">{{ user.username }}</span></a></li>
<li><a href="#">{{ 'profile.first_entry'|trans }} <span class="pull-right badge bg-purple">{{ stats.firstEntry|date_short }}</span></a></li>
{% if is_granted('view_rate_own_timesheet') %}
<li><a href="#">{{ 'label.hourlyRate'|trans }} <span class="pull-right badge bg-blue">{{ user.preferenceValue('hourly_rate') }}</span></a></li>
{% endif %}
<li><a href="#">{{ 'label.now'|trans }} <span class="pull-right badge bg-blue">{{ local_time|date_short }} {{ local_time|time }}</span></a></li>
</ul>
</div>
</div>
{% endmacro %}
{% macro profile_box(user, stats) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-primary">
<div class="box-body box-profile">
{{ macro.avatar(user.avatar, user.username, 'profile-user-img img-responsive img-circle') }}
<h3 class="profile-username text-center">{{ widgets.username(user) }}</h3>
<p class="text-muted text-center">{{ user.title }}</p>
<ul class="list-group list-group-unbordered">
<li class="list-group-item">
<b>{{ 'stats.durationMonth'|trans }}</b> <a class="pull-right">{{ stats.durationThisMonth|duration }}</a>
</li>
{% if is_granted('view_rate_own_timesheet') %}
<li class="list-group-item">
<b>{{ 'stats.amountMonth'|trans }}</b> <a class="pull-right">{{ stats.amountThisMonth|money }}</a>
</li>
{% endif %}
<li class="list-group-item">
<b>{{ 'stats.durationTotal'|trans }}</b> <a class="pull-right">{{ stats.durationTotal|duration }}</a>
</li>
{% if is_granted('view_rate_own_timesheet') %}
<li class="list-group-item">
<b>{{ 'stats.amountTotal'|trans }}</b> <a class="pull-right">{{ stats.amountTotal|money }}</a>
</li>
{% endif %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_list_unused(user, items, color) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user-2">
<!-- Add the bg color to the header using any of the bg-* classes -->
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<!-- /.widget-user-image -->
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="box-footer no-padding">
<ul class="nav nav-stacked">
{% for entry in items %}
<li><a href="{{ entry.url }}">{{ entry.title|trans }} <span class="pull-right badge bg-{{ entry.color }}">{{ entry.value }}</span></a></li>
{% endfor %}
</ul>
</div>
</div>
{% endmacro %}
{# -------------------------------- UNUSED FOR NOW -------------------------------- #}
{% macro profile_box_unused(user, stats, color) %}
{% import "@AdminLTE/Macros/default.html.twig" as macro %}
{% import "macros/widgets.html.twig" as widgets %}
<div class="box box-widget widget-user">
<div class="widget-user-header bg-{{ color|default(kimai_context.box_color) }}">
<h3 class="widget-user-username">{{ widgets.username(user) }}</h3>
<h5 class="widget-user-desc">{{ user.title }}</h5>
</div>
<div class="widget-user-image">
{{ macro.avatar(user.avatar, user.username) }}
</div>
<div class="box-footer">
<div class="row">
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationTotal|duration }}</h5>
<span class="description-text">{{ 'stats.durationTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountTotal|money }}</h5>
<span class="description-text">{{ 'stats.amountTotal'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.durationThisMonth|duration }}</h5>
<span class="description-text">{{ 'stats.durationMonth'|trans }}</span>
</div>
</div>
<div class="col-sm-3 border-right">
<div class="description-block">
<h5 class="description-header">{{ stats.amountThisMonth|money }}</h5>
<span class="description-text">{{ 'stats.amountMonth'|trans }}</span>
</div>
</div>
</div>
</div>
</div>
{% endmacro %}

View File

@@ -0,0 +1,97 @@
{% extends 'user/layout.html.twig' %}
{% import "macros/actions.html.twig" as actions %}
{% import "macros/widgets.html.twig" as widgets %}
{% block profile_content %}
{% if years is empty %}
{{ widgets.callout('warning', 'error.no_entries_found') }}
{% endif %}
<script type="text/javascript">
var userProfileChartOptions = {
maintainAspectRatio : true,
responsive : true,
legend : false,
barPercentage : 0.5,
categoryPercentage : 0.9,
scales: {
xAxes: [{
gridLines: {
display: false
}
}],
yAxes: [{
gridLines: {
display: true,
color: 'rgba(0,0,0,.05)',
lineWidth: 1
}
}]
}
};
var userProfileChartLabels = [
{% for i in 1..12 %}
'{{ ('month.' ~i)|trans }}'
{% if not loop.last %},{% endif %}
{% endfor %}
];
</script>
{% for year,yearStat in years %}
{% embed '@AdminLTE/Widgets/box-widget.html.twig' %}
{% block box_title %}{{ year }}{% endblock %}
{% block box_body %}
<div class="chart">
<canvas id="userProfileChart{{ year }}" style="height: 200px;"></canvas>
</div>
<script type="text/javascript">
$(document).ready(function () {
var userProfileChartData{{ year }} = {
labels: userProfileChartLabels,
datasets: [
{
label: '{{ year }}',
backgroundColor : '#00a65a',
borderColor : '#00a65a',
data: [
{% for month in yearStat.months %}
{{ (month.totalDuration / 3600)|round }}
{% if not loop.last %},{% endif %}
{% endfor %}
]
}
]
};
var userProfileChartCanvas{{ year }} = $("#userProfileChart{{ year }}").get(0).getContext("2d");
{% if tab == 'charts' %}
var userProfileChart{{ year }} = new Chart(
userProfileChartCanvas{{ year }}, {
type: 'bar',
data: userProfileChartData{{ year }},
options: userProfileChartOptions
}
);
{% endif %}
$('a[href="#charts"]').on('shown.bs.tab', function(event){
var userProfileChart{{ year }} = new Chart(
userProfileChartCanvas{{ year }}, {
type: 'bar',
data: userProfileChartData{{ year }},
options: userProfileChartOptions
}
);
});
});
</script>
{% endblock %}
{% endembed %}
{% endfor %}
{% endblock %}

View File

@@ -38,7 +38,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
->setHourlyRate(true)
->setAmount(10)
->setUser($this->getUserByRole($em, $role))
->setStartDate((new \DateTime('-10 days'))->setTime(0, 0, 1))
->setStartDate((new \DateTime('first day of this month'))->setTime(0, 0, 1))
->setAllowEmptyDescriptions(false)
;
$this->importFixture($em, $fixture);
@@ -124,9 +124,9 @@ class TimesheetControllerTest extends APIControllerBaseTest
public function testGetCollectionWithQuery()
{
$begin = new \DateTime('-10 days');
$begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0);
$end = new \DateTime();
$end = new \DateTime('last day of this month');
$end->setTime(23, 59, 59);
$query = [
@@ -162,14 +162,14 @@ class TimesheetControllerTest extends APIControllerBaseTest
->setExported(true)
->setAmount(7)
->setUser($this->getUserByRole($em, User::ROLE_USER))
->setStartDate(new \DateTime('-10 days'))
->setStartDate(new \DateTime('first day of this month'))
->setAllowEmptyDescriptions(false)
;
$this->importFixture($em, $fixture);
$begin = new \DateTime('-10 days');
$begin = new \DateTime('first day of this month');
$begin->setTime(0, 0, 0);
$end = new \DateTime();
$end = new \DateTime('last day of this month');
$end->setTime(23, 59, 59);
$query = [

View File

@@ -17,21 +17,38 @@ use App\Entity\User;
*/
class AboutControllerTest extends ControllerBaseTest
{
public function testIsSecure()
public function testDebugIsSecure()
{
$this->assertUrlIsSecured('/admin/about');
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/admin/about');
$this->assertUrlIsSecured('/about/debug');
$this->assertUrlIsSecuredForRole(User::ROLE_ADMIN, '/about/debug');
}
public function testIndexAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->assertAccessIsGranted($client, '/about');
$result = $client->getCrawler()->filter('ul.nav.nav-stacked li a');
$this->assertEquals(3, count($result));
$result = $client->getCrawler()->filter('div.box-body pre');
$this->assertEquals(1, count($result));
$this->assertContains('MIT License', $result->text());
}
public function testDebugAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
$this->assertAccessIsGranted($client, '/admin/about');
$this->assertAccessIsGranted($client, '/about/debug');
$result = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav.nav-tabs li');
$this->assertEquals(3, count($result));
$content = $client->getResponse()->getContent();
$this->assertContains('<h3 class="box-title">Environment</h3>', $content);
$this->assertContains('<h3 class="box-title">PHP</h3>', $content);
$this->assertContains('<h3 class="box-title">Server</h3>', $content);
$result = $client->getCrawler()->filter('div.nav-tabs-custom div.tab-content div.tab-pane');
$this->assertEquals(3, count($result));
$this->assertContains('Actions', $content);
$this->assertContains('', $content);
$this->assertContains('PHP', $content);
$this->assertContains('<a href="/en/about/flush-cache"', $content);
}
}

View File

@@ -37,4 +37,25 @@ class DashboardControllerTest extends ControllerBaseTest
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertMainContentClass($client, 'dashboard');
}
/**
* This is not a test for the dashbaord, but for the general layout
*/
public function testUserMenuIsAvailable()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->request($client, '/dashboard/');
$this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
$this->assertContains('<li class="dropdown user-menu">', $content);
$this->assertContains('<a href="/en/profile/' . $user->getUsername() . '">', $content);
$this->assertContains('<a href="/en/profile/' . $user->getUsername() . '/prefs">', $content);
$this->assertContains('<a href="/en/logout">', $content);
}
}

View File

@@ -12,6 +12,8 @@ namespace App\Tests\Controller;
use App\DataFixtures\UserFixtures;
use App\Entity\User;
use App\Entity\UserPreference;
use App\Tests\DataFixtures\TimesheetFixtures;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
/**
@@ -25,12 +27,75 @@ class ProfileControllerTest extends ControllerBaseTest
$this->assertUrlIsSecured('/profile/' . UserFixtures::USERNAME_USER);
}
public function testIndexActionWithoutData()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
$this->assertTrue($client->getResponse()->isSuccessful());
$this->assertHasNoEntriesWithFilter($client);
$this->assertHasProfileBox($client, UserFixtures::USERNAME_USER);
$this->assertHasAboutMeBox($client, UserFixtures::USERNAME_USER);
}
public function testIndexAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$dates = [
new \DateTime('-10 days'),
new \DateTime('-1 year'),
];
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
foreach ($dates as $start) {
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate($start);
$this->importFixture($em, $fixture);
}
$this->request($client, '/profile/' . UserFixtures::USERNAME_USER);
$this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
foreach ($dates as $start) {
$year = $start->format('Y');
$this->assertContains('<h3 class="box-title">' . $year . '</h3>', $content);
$this->assertContains('var userProfileChart' . $year . ' = new Chart(', $content);
}
$this->assertHasProfileBox($client, UserFixtures::USERNAME_USER);
$this->assertHasAboutMeBox($client, UserFixtures::USERNAME_USER);
}
protected function assertHasProfileBox(Client $client, string $username)
{
$profileBox = $client->getCrawler()->filter('div.box-body.box-profile');
$this->assertEquals(1, $profileBox->count());
$profileAvatar = $profileBox->filter('img.profile-user-img');
$this->assertEquals(1, $profileAvatar->count());
$alt = $profileAvatar->attr('alt');
$this->assertEquals($username, $alt);
}
protected function assertHasAboutMeBox(Client $client, string $username)
{
$content = $client->getResponse()->getContent();
$this->assertContains('<h3 class="box-title">About me</h3>', $content);
$this->assertContains('<span class="pull-right badge bg-blue">' . $username . '</span>', $content);
}
public function getTabTestData()
{
$userTabs = ['#charts', '#settings', '#password', '#api-token', '#preferences'];
$userTabs = ['#settings', '#password', '#api-token'];
return [
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#charts', '#settings', '#password', '#api-token', '#preferences']],
[User::ROLE_USER, UserFixtures::USERNAME_USER, ['#settings', '#password', '#api-token']],
[User::ROLE_SUPER_ADMIN, UserFixtures::USERNAME_SUPER_ADMIN, array_merge($userTabs, ['#roles'])],
];
}
@@ -38,10 +103,10 @@ class ProfileControllerTest extends ControllerBaseTest
/**
* @dataProvider getTabTestData
*/
public function testIndexAction($role, $username, $expectedTabs)
public function testEditActionTabs($role, $username, $expectedTabs)
{
$client = $this->getClientForAuthenticatedUser($role);
$this->request($client, '/profile/' . $username);
$this->request($client, '/profile/' . $username . '/edit');
$this->assertTrue($client->getResponse()->isSuccessful());
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');

View File

@@ -1,40 +0,0 @@
<?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\Controller;
use App\Constants;
use App\Entity\User;
/**
* @coversDefaultClass \App\Controller\SidebarController
* @group integration
*/
class SidebarControllerTest extends ControllerBaseTest
{
public function testSettingsAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$user = $this->getUserByRole($em, User::ROLE_USER);
$this->request($client, '/sidebar/settings');
$this->assertTrue($client->getResponse()->isSuccessful());
$content = $client->getResponse()->getContent();
$this->assertContains('<ul class="control-sidebar-menu">', $content);
$this->assertContains('<a href="/en/profile/' . $user->getUsername() . '">', $content);
$this->assertContains('<a href="/en/profile/' . $user->getUsername() . '/edit">', $content);
$this->assertContains('<a href="/en/profile/' . $user->getUsername() . '/prefs">', $content);
$this->assertContains('<a href="' . Constants::HOMEPAGE . '/documentation/" target="_blank">', $content);
$this->assertContains('<a href="/en/logout">', $content);
}
}

View File

@@ -46,18 +46,19 @@ class TimesheetControllerTest extends ControllerBaseTest
public function testIndexActionWithQuery()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
$start = new \DateTime('first day of this month');
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$fixture = new TimesheetFixtures();
$fixture->setAmount(5);
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
$fixture->setStartDate(new \DateTime('-10 days'));
$fixture->setStartDate($start);
$this->importFixture($em, $fixture);
$this->request($client, '/timesheet/');
$this->assertTrue($client->getResponse()->isSuccessful());
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.navbar-form')->form();
$client->submit($form, [

View File

@@ -7,12 +7,11 @@
* file that was distributed with this source code.
*/
namespace App\Tests\Controller\Admin;
namespace App\Tests\Controller;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Form\Type\DateRangeType;
use App\Tests\Controller\ControllerBaseTest;
use App\Tests\DataFixtures\TimesheetFixtures;
/**
@@ -48,19 +47,20 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
public function testIndexActionWithQuery()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$start = new \DateTime('first day of this month');
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
$user = $this->getUserByRole($em, User::ROLE_USER);
$fixture = new TimesheetFixtures();
$fixture->setAmount(10);
$fixture->setUser($user);
$fixture->setStartDate(new \DateTime('-10 days'));
$fixture->setStartDate($start);
$this->importFixture($em, $fixture);
$this->request($client, '/team/timesheet/');
$this->assertTrue($client->getResponse()->isSuccessful());
$dateRange = (new \DateTime('-10 days'))->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime())->format('Y-m-d');
$dateRange = ($start)->format('Y-m-d') . DateRangeType::DATE_SPACER . (new \DateTime('last day of this month'))->format('Y-m-d');
$form = $client->getCrawler()->filter('form.navbar-form')->form();
$client->submit($form, [

View File

@@ -52,7 +52,7 @@ class UserControllerTest extends ControllerBaseTest
$this->assertIsRedirect($client, $this->createUrl('/profile/' . urlencode($username) . '/edit'));
$client->followRedirect();
$expectedTabs = ['#charts', '#settings', '#password', '#api-token', '#roles', '#preferences'];
$expectedTabs = ['#settings', '#password', '#api-token', '#roles'];
$tabs = $client->getCrawler()->filter('div.nav-tabs-custom ul.nav-tabs li');
$this->assertEquals(count($expectedTabs), $tabs->count());

View File

@@ -236,7 +236,7 @@ class TimesheetFixtures extends Fixture
{
$start = \DateTime::createFromFormat('Y-m-d', $this->startDate);
$start->modify("+ $i days");
$start->modify('+ ' . rand(1, 172.800) . ' seconds'); // up to 2 days
$start->modify('+ ' . rand(1, 172800) . ' seconds'); // up to 2 days
return $start;
}

View File

@@ -215,7 +215,8 @@ class ExtensionsTest extends TestCase
$icons = [
'user', 'customer', 'project', 'activity', 'admin', 'invoice', 'timesheet', 'dashboard', 'logout', 'trash',
'delete', 'repeat', 'edit', 'manual', 'help', 'start', 'start-small', 'stop', 'stop-small', 'filter',
'create', 'list', 'print', 'visibility', 'calendar', 'money', 'duration', 'download', 'copy', 'settings'
'create', 'list', 'print', 'visibility', 'calendar', 'money', 'duration', 'download', 'copy', 'settings',
'export', 'pdf', 'csv', 'ods', 'xlsx', 'on', 'off', 'audit', 'home', 'shop', 'about', 'debug', 'profile-stats'
];
// test pre-defined icons

11
translations/about.ar.xliff Executable file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="ar" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="help">
<source>help</source>
<target>مساعدة</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="about.en.xliff">
<file source-language="en" target-language="de" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="about.title">
<source>about.title</source>
@@ -10,13 +10,17 @@
<source>about.subtitle</source>
<target>System Informationen</target>
</trans-unit>
<trans-unit id="tab.system">
<source>tab.system</source>
<target>System Info</target>
<trans-unit id="support">
<source>support</source>
<target>Support</target>
</trans-unit>
<trans-unit id="tab.license">
<source>tab.license</source>
<target>Kimai Lizenz</target>
<trans-unit id="website">
<source>website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="help">
<source>help</source>
<target>Dokumentation</target>
</trans-unit>
<trans-unit id="tab.thanks">
<source>tab.thanks</source>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="about.en.xliff">
<file source-language="en" target-language="en" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="about.title">
<source>about.title</source>
@@ -10,13 +10,17 @@
<source>about.subtitle</source>
<target>System information</target>
</trans-unit>
<trans-unit id="tab.system">
<source>tab.system</source>
<target>System Info</target>
<trans-unit id="support">
<source>support</source>
<target>Support</target>
</trans-unit>
<trans-unit id="tab.license">
<source>tab.license</source>
<target>Kimai License</target>
<trans-unit id="website">
<source>website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="help">
<source>help</source>
<target>Documentation</target>
</trans-unit>
<trans-unit id="tab.thanks">
<source>tab.thanks</source>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="es" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="help">
<source>help</source>
<target>Ayuda</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="help">
<source>help</source>
<target>Aide</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="hu" datatype="plaintext" original="about.en.xliff">
<file source-language="en" target-language="hu" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="about.title">
<source>about.title</source>
@@ -10,6 +10,14 @@
<source>about.subtitle</source>
<target>Rendszerinformáció</target>
</trans-unit>
<trans-unit id="website">
<source>website</source>
<target>Weboldal</target>
</trans-unit>
<trans-unit id="help">
<source>help</source>
<target>Súgó</target>
</trans-unit>
</body>
</file>
</xliff>

11
translations/about.it.xliff Executable file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="it" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="help">
<source>help</source>
<target>Aiuto</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -10,6 +10,14 @@
<source>about.subtitle</source>
<target>Informação do Sistema</target>
</trans-unit>
<trans-unit id="website">
<source>website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="help">
<source>help</source>
<target>Ajuda</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="ru" datatype="plaintext" original="about.en.xliff">
<body>
<trans-unit id="help">
<source>help</source>
<target>Помощь</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -183,6 +183,10 @@
<source>label.hourlyRate</source>
<target>بالساعة</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>بالساعة</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai-Theme</target>
@@ -331,10 +335,6 @@
<source>profile.first_entry</source>
<target>العمل منذ</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>إحصاء سنوي</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>الملف الشخصي</target>

View File

@@ -201,6 +201,10 @@
<source>label.hourlyRate</source>
<target>Stundenlohn</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Stundenlohn</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai-Theme</target>
@@ -353,10 +357,6 @@
<source>profile.first_entry</source>
<target>Arbeitet seit</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Jahresstatistik</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Profil</target>
@@ -406,10 +406,6 @@
<source>label.timesheet.daily_stats</source>
<target>Tägliche Statistiken im Timesheet anzeigen</target>
</trans-unit>
<trans-unit id="label.now">
<source>label.now</source>
<target>Aktuelle Zeit</target>
</trans-unit>
<!--
User timesheet calendar

View File

@@ -201,6 +201,10 @@
<source>label.hourlyRate</source>
<target>Hourly rate</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Hourly rate</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai-Theme</target>
@@ -353,10 +357,6 @@
<source>profile.first_entry</source>
<target>Working since</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Yearly statistic</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Profile</target>
@@ -406,10 +406,6 @@
<source>label.timesheet.daily_stats</source>
<target>Show daily stats in timesheet</target>
</trans-unit>
<trans-unit id="label.now">
<source>label.now</source>
<target>Current time</target>
</trans-unit>
<!--
User timesheet calendar

View File

@@ -191,6 +191,10 @@
<source>label.hourlyRate</source>
<target>Tarifa por hora</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Tarifa por hora</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Tema-Kimai</target>
@@ -335,10 +339,6 @@
<source>profile.first_entry</source>
<target>Activo desde</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Estadística anual</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Perfil</target>

View File

@@ -191,6 +191,10 @@
<source>label.hourlyRate</source>
<target>Tarif horaire</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Tarif horaire</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Thème</target>
@@ -335,10 +339,6 @@
<source>profile.first_entry</source>
<target>Activités suivies depuis</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Statistiques annuelles</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Profil</target>

View File

@@ -193,6 +193,10 @@
<source>label.hourlyRate</source>
<target>Órabér</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Órabér</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai téma</target>
@@ -345,10 +349,6 @@
<source>profile.first_entry</source>
<target>Munka kezdete</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Éves statisztika</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Profil</target>
@@ -398,10 +398,6 @@
<source>label.timesheet.daily_stats</source>
<target>Napi statisztika megjelenítése a rögzített óráknál</target>
</trans-unit>
<trans-unit id="label.now">
<source>label.now</source>
<target>Pillanatnyi idő</target>
</trans-unit>
<!--
User timesheet calendar

View File

@@ -183,6 +183,10 @@
<source>label.hourlyRate</source>
<target>Tariffa oraria</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Tariffa oraria</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai-Theme</target>
@@ -327,10 +331,6 @@
<source>profile.first_entry</source>
<target>Profilo registrato il</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Statistiche annuali</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Profilo</target>

View File

@@ -183,6 +183,10 @@
<source>label.hourlyRate</source>
<target>Taxa horária</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Taxa horária</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Tema Kimai</target>
@@ -331,10 +335,6 @@
<source>profile.first_entry</source>
<target>Trabalhando desde</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Estatística anual</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Perfil</target>

View File

@@ -183,6 +183,10 @@
<source>label.hourlyRate</source>
<target>Почасовая заработная плата</target>
</trans-unit>
<trans-unit id="label.hourly_rate">
<source>label.hourly_rate</source>
<target>Почасовая заработная плата</target>
</trans-unit>
<trans-unit id="label.skin">
<source>label.skin</source>
<target>Kimai-Theme</target>
@@ -327,10 +331,6 @@
<source>profile.first_entry</source>
<target>Первая запись</target>
</trans-unit>
<trans-unit id="profile.tab_monthly">
<source>profile.tab_monthly</source>
<target>Годовая статистика</target>
</trans-unit>
<trans-unit id="profile.settings">
<source>profile.settings</source>
<target>Профиль</target>

View File

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="ar" datatype="plaintext" original="none">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[شكرًا لك على استخدام أداة تتبع الوقت مفتوحة المصدر Kimai.
هذا الإصدار الجديد حاليًا في مرحلة مبكرة من التطوير وقد تم نشره مسبقًا لجمع التعليقات من مستخدميه ودمج أفكارهم في التخطيط الإضافي.
لمساعدتي في تحسين Kimai, الرجاء إرسال رسالتك على موقع الويب المرتبط. سواء كان لديك أسئلة أو للتبليغ عن خطأ أو عطل أو أفكار للتحسينات ، فإن تعليقاتك ذات قيمة!
]]></target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>مساعدة</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="de" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Danke für die Nutzung der Open-Source Zeiterfassung Kimai.
Diese neue Version ist aktuell noch in einer frühen Phase der Entwicklung und wurde vorab veröffentlicht, um frühzeitig Rückmeldungen ihrer Benutzer zu sammeln und deren Ideen in die weitere Planung zu integrieren.
Um bei der Verbesserung von Kimai zu helfen, schicken Sie mir bitte eine Nachricht auf der verlinkten Webseite. Egal ob Fragen, Fehlermeldungen oder Ideen für Erweiterungen, Ihr Feedback ist wertvoll!
]]></target>
</trans-unit>
<trans-unit id="home.github">
<source>home.github</source>
<target>Kimai - Projekt Webseite</target>
</trans-unit>
<trans-unit id="home.website">
<source>home.website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Hilfe</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="en" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Thank you for using the open source time-tracker Kimai.
This new version is currently in an early stage of development and has been published in advance to collect feedback from its users and integrate their ideas into further planning.
To help me improve Kimai, please send your message on the linked website. Whether you have questions, error messages or ideas for enhancements, your feedback is valuable!
]]></target>
</trans-unit>
<trans-unit id="home.github">
<source>home.github</source>
<target>Kimai - Project website</target>
</trans-unit>
<trans-unit id="home.website">
<source>home.website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Help</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="es" datatype="plaintext" original="none">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Gracias por usar el control de tiempo ppen source Kimai.
Esta nueva versión se encuentra en una etapa inicial de desarrollo y ha sido publicada con la intención de recolectar feedback de los usuarios para incluir sus ideas en planes futuros..
Para ayudar a mejorar Kimai, por favor enviar sus comentarios al sitio. Sus comentarios, ideas para mejorar o reportes de errores son valiosos y agradezco el tiempo para enviárnoslos!
]]></target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Ayuda</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="fr" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Merci d'utiliser ce logiciel libre de suivi de temps Kimai.
Cette nouvelle version est actuellement à un stade précoce de développement et a été publié à l'avance afin de recueillir les commentaires de ses utilisateurs et d'intégrer leurs idées dans la planification à venir.
Pour m'aider à améliorer Kimai, merci de poster votre message sur le site lié. Que vous ayez des questions, des messages ou des idées d'améliorations, vos retours sont précieux !
]]></target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Aide</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="hu" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Köszönjük, hogy a nyílt forráskódú Kimai időrögzító szoftvert használod.
Ez a jelenlegi verzió még a fejlesztés korai szakaszában jár és elsősorban azért lett publikálva, hogy visszajelzéseket gyűjtsön a felhasználóktól, hogy az ötleteiket később figyelembe lehessen venni a további tervezéshez.
A Kimai fejlesztéséhez nagyban hozzájárulhatsz, ha üzenetet küldesz nekünk a lenti oldalon. Akár csak kérdésed van, vagy egy jó ötleted ami javítaná az oldalt, minden visszajelzés fontos nekünk!
Ha a magyar fordításban találsz hibát, kérlek azt is jelezd a lenti oldalon.
]]></target>
</trans-unit>
<trans-unit id="home.github">
<source>home.github</source>
<target>Kimai projekt oldala</target>
</trans-unit>
<trans-unit id="home.website">
<source>home.website</source>
<target>Weboldal</target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Súgó</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="it" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Thank you for using the open source time-tracker Kimai.
This new version is currently in an early stage of development and has been published in advance to collect feedback from its users and integrate their ideas into further planning.
To help me improve Kimai, please send your message on the linked website. Whether you have questions, error messages or ideas for enhancements, your feedback is valuable!
]]></target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Aiuto</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="pt-BR" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Obrigado por usar o rastreador de tempo de código aberto Kimai.
Esta nova versão está atualmente em um estágio inicial de desenvolvimento e foi publicada com antecedência para coletar feedback de seus usuários e integrar suas ideias em planejamento adicional.
Para me ajudar a melhorar o Kimai, por favor envie sua mensagem no site vinculado. Se você tiver dúvidas, mensagens de erro ou ideias para melhorias, seu feedback é valioso!
]]></target>
</trans-unit>
<trans-unit id="home.github">
<source>home.github</source>
<target>Kimai - Project website</target>
</trans-unit>
<trans-unit id="home.website">
<source>home.website</source>
<target>Homepage</target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Ajuda</target>
</trans-unit>
</body>
</file>
</xliff>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="en" target-language="ru" datatype="plaintext" original="sidebar.en.xliff">
<body>
<trans-unit id="home.betainfo">
<source>home.betainfo</source>
<target><![CDATA[Спасибо за использование open source Программы для учета времени Kimai.
Данная Версия пока еще находится на ранней стадии своей разработки. Она была выпущена с целью получения предварительных отзывов от пользователей и их последующего учета при дальнейшем планировании функций.
Для помощи в усовершенствовании Kimai, пожалуйства, отправляйте свои сообщения на страницу под ссылкой. Приветствуются любые вопросы, идеи по разработке, в частности на тему дополнительных функций, а также сообщения об ошибках. Нам очень важна Ваша обратная связь!
]]></target>
</trans-unit>
<trans-unit id="home.help">
<source>home.help</source>
<target>Помощь</target>
</trans-unit>
</body>
</file>
</xliff>