Only one running timesheet: automatically stop others (#386)
This commit is contained in:
@@ -35,6 +35,18 @@ kimai:
|
|||||||
# days: ['saturday','sunday']
|
# days: ['saturday','sunday']
|
||||||
# factor: 1.5
|
# factor: 1.5
|
||||||
|
|
||||||
|
# If you want to limit the max. active entries per user, you can do it here.
|
||||||
|
#
|
||||||
|
# The soft_limit is used as theme setting (formerly "kimai.theme.active_warning"):
|
||||||
|
# display a warning color if the user has at least X active recordings
|
||||||
|
#
|
||||||
|
# The hard_limit is used to detect how many active records are allowed per user:
|
||||||
|
# By default a user can only have one active time-record. The first one is stopped when the new one starts.
|
||||||
|
# When hard_limit is > 1: as soon as the limit is reached and warning is shown and the user has to stop one active entry.
|
||||||
|
active_entries:
|
||||||
|
soft_limit: 1
|
||||||
|
hard_limit: 3
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Invoice management
|
# Invoice management
|
||||||
#invoice:
|
#invoice:
|
||||||
@@ -147,8 +159,6 @@ kimai:
|
|||||||
# BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
# BETA test: If you set this to 'selectpicker' the customer/project/activity select boxes will be transformed
|
||||||
# into a searchable and javascript enhanced input type
|
# into a searchable and javascript enhanced input type
|
||||||
select_type: ~
|
select_type: ~
|
||||||
# display a warning color if the user has at least X active recordings
|
|
||||||
active_warning: 3
|
|
||||||
# fallback color for all widgets that don't have a dedicated color
|
# fallback color for all widgets that don't have a dedicated color
|
||||||
# possible options: blue, black, purple, yellow, red, green
|
# possible options: blue, black, purple, yellow, red, green
|
||||||
box_color: 'green'
|
box_color: 'green'
|
||||||
@@ -211,3 +221,10 @@ kimai:
|
|||||||
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
|
activeUsersYear: { title: stats.userActiveYear, query: users, begin: '01 january this year 00:00:00', end: '31 december this year 23:59:59', icon: user, color: yellow }
|
||||||
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
activeUsersTotal: { title: stats.userActiveTotal, query: users, icon: user, color: red }
|
||||||
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
activeRecordings: { title: stats.activeRecordings, query: active, icon: duration, color: red }
|
||||||
|
|
||||||
|
# Default settings used to populate forms
|
||||||
|
#defaults:
|
||||||
|
# customer:
|
||||||
|
# timezone: Europe/Berlin
|
||||||
|
# country: DE
|
||||||
|
# currency: EUR
|
||||||
@@ -14,7 +14,7 @@ services:
|
|||||||
# The best practice is to be explicit about your dependencies anyway.
|
# The best practice is to be explicit about your dependencies anyway.
|
||||||
bind:
|
bind:
|
||||||
$projectDirectory: "%kernel.project_dir%"
|
$projectDirectory: "%kernel.project_dir%"
|
||||||
$durationOnly: "%kimai.timesheet.duration_only%"
|
$hardLimit: "%kimai.timesheet.active_entries.hard_limit%"
|
||||||
|
|
||||||
# makes classes in src/ available to be used as services
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
@@ -93,6 +93,10 @@ services:
|
|||||||
tags:
|
tags:
|
||||||
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
- { name: form.type_extension, extended_type: Symfony\Bridge\Doctrine\Form\Type\EntityType }
|
||||||
|
|
||||||
|
App\Form\TimesheetEditForm:
|
||||||
|
arguments:
|
||||||
|
$durationOnly: "%kimai.timesheet.duration_only%"
|
||||||
|
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
# THEME
|
# THEME
|
||||||
# ================================================================================
|
# ================================================================================
|
||||||
|
|||||||
@@ -41,14 +41,21 @@ class TimesheetController extends BaseApiController
|
|||||||
*/
|
*/
|
||||||
protected $viewHandler;
|
protected $viewHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $hardLimit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ViewHandlerInterface $viewHandler
|
* @param ViewHandlerInterface $viewHandler
|
||||||
* @param TimesheetRepository $repository
|
* @param TimesheetRepository $repository
|
||||||
|
* @param int $hardLimit
|
||||||
*/
|
*/
|
||||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository)
|
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, int $hardLimit)
|
||||||
{
|
{
|
||||||
$this->viewHandler = $viewHandler;
|
$this->viewHandler = $viewHandler;
|
||||||
$this->repository = $repository;
|
$this->repository = $repository;
|
||||||
|
$this->hardLimit = $hardLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -161,8 +168,7 @@ class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
'method' => 'POST',
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
'duration_only' => false,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$form->setData($timesheet);
|
$form->setData($timesheet);
|
||||||
@@ -177,6 +183,21 @@ class TimesheetController extends BaseApiController
|
|||||||
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
|
return new Response('You are not allowed to start this timesheet record', Response::HTTP_BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($form->has('duration')) {
|
||||||
|
$duration = $form->get('duration')->getData();
|
||||||
|
if ($duration > 0) {
|
||||||
|
/** @var Timesheet $record */
|
||||||
|
$record = $form->getData();
|
||||||
|
$end = clone $record->getBegin();
|
||||||
|
$end->modify('+ ' . $duration . 'seconds');
|
||||||
|
$record->setEnd($end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->repository->stopActiveEntries(
|
||||||
|
$timesheet->getUser(),
|
||||||
|
$this->hardLimit
|
||||||
|
);
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
$entityManager = $this->getDoctrine()->getManager();
|
||||||
$entityManager->persist($timesheet);
|
$entityManager->persist($timesheet);
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|||||||
@@ -42,15 +42,7 @@ abstract class AbstractController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function flashSuccess($translationKey, $parameter = [])
|
protected function flashSuccess($translationKey, $parameter = [])
|
||||||
{
|
{
|
||||||
if (!empty($parameter)) {
|
$this->addFlashTranslated(self::FLASH_SUCCESS, $translationKey, $parameter);
|
||||||
$translationKey = $this->getTranslator()->trans(
|
|
||||||
$translationKey,
|
|
||||||
$parameter,
|
|
||||||
self::DOMAIN_FLASH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->addFlash(self::FLASH_SUCCESS, $translationKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,15 +53,7 @@ abstract class AbstractController extends Controller
|
|||||||
*/
|
*/
|
||||||
protected function flashWarning($translationKey, $parameter = [])
|
protected function flashWarning($translationKey, $parameter = [])
|
||||||
{
|
{
|
||||||
if (!empty($parameter)) {
|
$this->addFlashTranslated(self::FLASH_WARNING, $translationKey, $parameter);
|
||||||
$translationKey = $this->getTranslator()->trans(
|
|
||||||
$translationKey,
|
|
||||||
$parameter,
|
|
||||||
self::DOMAIN_FLASH
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->addFlash(self::FLASH_WARNING, $translationKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -79,15 +63,30 @@ abstract class AbstractController extends Controller
|
|||||||
* @param array $parameter
|
* @param array $parameter
|
||||||
*/
|
*/
|
||||||
protected function flashError($translationKey, $parameter = [])
|
protected function flashError($translationKey, $parameter = [])
|
||||||
|
{
|
||||||
|
$this->addFlashTranslated(self::FLASH_ERROR, $translationKey, $parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a fully translated (both $message and all keys in $parameter) flash message to the stack.
|
||||||
|
*
|
||||||
|
* @param string $type
|
||||||
|
* @param string $message
|
||||||
|
* @param array $parameter
|
||||||
|
*/
|
||||||
|
protected function addFlashTranslated(string $type, string $message, array $parameter = [])
|
||||||
{
|
{
|
||||||
if (!empty($parameter)) {
|
if (!empty($parameter)) {
|
||||||
$translationKey = $this->getTranslator()->trans(
|
foreach ($parameter as $key => $value) {
|
||||||
$translationKey,
|
$parameter[$key] = $this->getTranslator()->trans($value, [], self::DOMAIN_FLASH);
|
||||||
|
}
|
||||||
|
$message = $this->getTranslator()->trans(
|
||||||
|
$message,
|
||||||
$parameter,
|
$parameter,
|
||||||
self::DOMAIN_FLASH
|
self::DOMAIN_FLASH
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->addFlash(self::FLASH_ERROR, $translationKey);
|
$this->addFlash($type, $message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,15 +30,6 @@ class TimesheetController extends AbstractController
|
|||||||
{
|
{
|
||||||
use TimesheetControllerTrait;
|
use TimesheetControllerTrait;
|
||||||
|
|
||||||
/**
|
|
||||||
* TimesheetController constructor.
|
|
||||||
* @param bool $durationOnly
|
|
||||||
*/
|
|
||||||
public function __construct(bool $durationOnly)
|
|
||||||
{
|
|
||||||
$this->setDurationMode($durationOnly);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
|
* @Route(path="/", defaults={"page": 1}, name="admin_timesheet", methods={"GET"})
|
||||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
|
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="admin_timesheet_paginated", methods={"GET"})
|
||||||
@@ -179,8 +170,6 @@ class TimesheetController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||||
'action' => $this->generateUrl('admin_timesheet_create'),
|
'action' => $this->generateUrl('admin_timesheet_create'),
|
||||||
'method' => 'POST',
|
|
||||||
'duration_only' => $this->isDurationOnlyMode(),
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'include_user' => true,
|
'include_user' => true,
|
||||||
]);
|
]);
|
||||||
@@ -198,8 +187,6 @@ class TimesheetController extends AbstractController
|
|||||||
'id' => $entry->getId(),
|
'id' => $entry->getId(),
|
||||||
'page' => $page
|
'page' => $page
|
||||||
]),
|
]),
|
||||||
'method' => 'POST',
|
|
||||||
'duration_only' => $this->isDurationOnlyMode(),
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'include_user' => true,
|
'include_user' => true,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ class TimesheetController extends AbstractController
|
|||||||
use TimesheetControllerTrait;
|
use TimesheetControllerTrait;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $durationOnly
|
* @param int $hardLimit
|
||||||
*/
|
*/
|
||||||
public function __construct(bool $durationOnly)
|
public function __construct(int $hardLimit)
|
||||||
{
|
{
|
||||||
$this->setDurationMode($durationOnly);
|
$this->setHardLimit($hardLimit);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -168,6 +168,8 @@ class TimesheetController extends AbstractController
|
|||||||
} else {
|
} else {
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
$entityManager = $this->getDoctrine()->getManager();
|
||||||
$entityManager->persist($entry);
|
$entityManager->persist($entry);
|
||||||
|
$this->stopActiveEntries($user);
|
||||||
|
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
$this->flashSuccess('timesheet.start.success');
|
$this->flashSuccess('timesheet.start.success');
|
||||||
}
|
}
|
||||||
@@ -238,9 +240,7 @@ class TimesheetController extends AbstractController
|
|||||||
{
|
{
|
||||||
return $this->createForm(TimesheetEditForm::class, $entry, [
|
return $this->createForm(TimesheetEditForm::class, $entry, [
|
||||||
'action' => $this->generateUrl('timesheet_create'),
|
'action' => $this->generateUrl('timesheet_create'),
|
||||||
'method' => 'POST',
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'duration_only' => $this->isDurationOnlyMode(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,9 +256,7 @@ class TimesheetController extends AbstractController
|
|||||||
'id' => $entry->getId(),
|
'id' => $entry->getId(),
|
||||||
'page' => $page
|
'page' => $page
|
||||||
]),
|
]),
|
||||||
'method' => 'POST',
|
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
'duration_only' => $this->isDurationOnlyMode(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
use App\Repository\TimesheetRepository;
|
use App\Repository\TimesheetRepository;
|
||||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
@@ -22,24 +23,24 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
trait TimesheetControllerTrait
|
trait TimesheetControllerTrait
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @var bool
|
* @var int
|
||||||
*/
|
*/
|
||||||
private $durationOnly = false;
|
private $hardLimit = 1;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bool $durationOnly
|
* @param int $hardLimit
|
||||||
*/
|
*/
|
||||||
protected function setDurationMode(bool $durationOnly)
|
protected function setHardLimit(int $hardLimit)
|
||||||
{
|
{
|
||||||
$this->durationOnly = $durationOnly;
|
$this->hardLimit = $hardLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return bool
|
* @return int
|
||||||
*/
|
*/
|
||||||
protected function isDurationOnlyMode()
|
protected function getHardLimit()
|
||||||
{
|
{
|
||||||
return $this->durationOnly;
|
return $this->hardLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,11 +170,16 @@ trait TimesheetControllerTrait
|
|||||||
}
|
}
|
||||||
|
|
||||||
$entityManager = $this->getDoctrine()->getManager();
|
$entityManager = $this->getDoctrine()->getManager();
|
||||||
$entityManager->persist($entry);
|
|
||||||
|
|
||||||
$entityManager->flush();
|
try {
|
||||||
|
$this->stopActiveEntries($entry->getUser());
|
||||||
|
$entityManager->persist($entry);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
$this->flashSuccess('action.update.success');
|
$this->flashSuccess('action.update.success');
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
$this->flashError('timesheet.start.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute($redirectRoute);
|
return $this->redirectToRoute($redirectRoute);
|
||||||
}
|
}
|
||||||
@@ -184,6 +190,17 @@ trait TimesheetControllerTrait
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @throws \App\Repository\RepositoryException
|
||||||
|
* @throws \Doctrine\ORM\ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
protected function stopActiveEntries(User $user)
|
||||||
|
{
|
||||||
|
$this->getRepository()->stopActiveEntries($user, $this->getHardLimit());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Timesheet $entry
|
* @param Timesheet $entry
|
||||||
* @return \Symfony\Component\Form\FormInterface
|
* @return \Symfony\Component\Form\FormInterface
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ class AppExtension extends Extension implements PrependExtensionInterface
|
|||||||
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
|
$container->setParameter('kimai.timesheet.rounding', $config['rounding']);
|
||||||
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
|
$container->setParameter('kimai.timesheet.duration_only', $config['duration_only']);
|
||||||
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
|
$container->setParameter('kimai.timesheet.markdown', $config['markdown_content']);
|
||||||
|
$container->setParameter('kimai.timesheet.active_entries.soft_limit', $config['active_entries']['soft_limit']);
|
||||||
|
$container->setParameter('kimai.timesheet.active_entries.hard_limit', $config['active_entries']['hard_limit']);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ class TwigContextCompilerPass implements CompilerPassInterface
|
|||||||
$theme = $container->getParameter('kimai.theme');
|
$theme = $container->getParameter('kimai.theme');
|
||||||
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
|
$durationOnly = $container->getParameter('kimai.timesheet.duration_only');
|
||||||
|
|
||||||
$twig->addMethodCall('addGlobal', ['kimai_context', $theme]);
|
$twig->addMethodCall('addGlobal', ['kimai_context', array_merge($theme, [
|
||||||
|
'active_warning' => $container->getParameter('kimai.timesheet.active_entries.soft_limit')
|
||||||
|
])]);
|
||||||
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
|
$twig->addMethodCall('addGlobal', ['duration_only', $durationOnly]);
|
||||||
|
|
||||||
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
if ($container->hasDefinition('twig.loader.native_filesystem')) {
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->defaultValue([])
|
->defaultValue([])
|
||||||
->end()
|
->end()
|
||||||
|
|
||||||
->arrayNode('rates')
|
->arrayNode('rates')
|
||||||
->requiresAtLeastOneElement()
|
->requiresAtLeastOneElement()
|
||||||
->useAttributeAsKey('key')
|
->useAttributeAsKey('key')
|
||||||
@@ -112,6 +111,30 @@ class Configuration implements ConfigurationInterface
|
|||||||
->end()
|
->end()
|
||||||
->defaultValue([])
|
->defaultValue([])
|
||||||
->end()
|
->end()
|
||||||
|
->arrayNode('active_entries')
|
||||||
|
->addDefaultsIfNotSet()
|
||||||
|
->children()
|
||||||
|
->integerNode('soft_limit')
|
||||||
|
->defaultValue(1)
|
||||||
|
->validate()
|
||||||
|
->ifTrue(function ($value) {
|
||||||
|
return $value <= 0;
|
||||||
|
})
|
||||||
|
->thenInvalid('The soft_limit must be at least 1')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->integerNode('hard_limit')
|
||||||
|
->defaultValue(1)
|
||||||
|
->validate()
|
||||||
|
->ifTrue(function ($value) {
|
||||||
|
return $value <= 0;
|
||||||
|
})
|
||||||
|
->thenInvalid('The hard_limit must be at least 1')
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
|
->end()
|
||||||
->end()
|
->end()
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -212,6 +235,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->children()
|
->children()
|
||||||
->integerNode('active_warning')
|
->integerNode('active_warning')
|
||||||
->defaultValue(3)
|
->defaultValue(3)
|
||||||
|
->setDeprecated('The node "%node%" at path "%path%" is deprecated, please use "kimai.timesheet.active_entries.soft_limit" instead.')
|
||||||
->end()
|
->end()
|
||||||
->scalarNode('box_color')
|
->scalarNode('box_color')
|
||||||
->defaultValue('green')
|
->defaultValue('green')
|
||||||
|
|||||||
@@ -36,19 +36,27 @@ class TimesheetEditForm extends AbstractType
|
|||||||
* @var CustomerRepository
|
* @var CustomerRepository
|
||||||
*/
|
*/
|
||||||
private $customers;
|
private $customers;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var ProjectRepository
|
* @var ProjectRepository
|
||||||
*/
|
*/
|
||||||
private $projects;
|
private $projects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $durationOnly = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param CustomerRepository $customer
|
* @param CustomerRepository $customer
|
||||||
* @param ProjectRepository $project
|
* @param ProjectRepository $project
|
||||||
|
* @param bool $durationOnly
|
||||||
*/
|
*/
|
||||||
public function __construct(CustomerRepository $customer, ProjectRepository $project)
|
public function __construct(CustomerRepository $customer, ProjectRepository $project, bool $durationOnly)
|
||||||
{
|
{
|
||||||
$this->customers = $customer;
|
$this->customers = $customer;
|
||||||
$this->projects = $project;
|
$this->projects = $project;
|
||||||
|
$this->durationOnly = $durationOnly;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -227,10 +235,11 @@ class TimesheetEditForm extends AbstractType
|
|||||||
'csrf_protection' => true,
|
'csrf_protection' => true,
|
||||||
'csrf_field_name' => '_token',
|
'csrf_field_name' => '_token',
|
||||||
'csrf_token_id' => 'timesheet_edit',
|
'csrf_token_id' => 'timesheet_edit',
|
||||||
'duration_only' => false,
|
'duration_only' => $this->durationOnly,
|
||||||
'include_user' => false,
|
'include_user' => false,
|
||||||
'include_rate' => true,
|
'include_rate' => true,
|
||||||
'docu_chapter' => 'timesheet',
|
'docu_chapter' => 'timesheet',
|
||||||
|
'method' => 'POST',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -260,6 +260,42 @@ class TimesheetRepository extends AbstractRepository
|
|||||||
return $qb->getQuery()->execute($params);
|
return $qb->getQuery()->execute($params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param User $user
|
||||||
|
* @param int $limit
|
||||||
|
* @return int
|
||||||
|
* @throws RepositoryException
|
||||||
|
* @throws \Doctrine\ORM\ORMException
|
||||||
|
* @throws \Doctrine\ORM\OptimisticLockException
|
||||||
|
*/
|
||||||
|
public function stopActiveEntries(User $user, int $hardLimit)
|
||||||
|
{
|
||||||
|
$counter = 0;
|
||||||
|
$activeEntries = $this->getActiveEntries($user);
|
||||||
|
|
||||||
|
// reduce limit by one:
|
||||||
|
// this method is only called when a new entry is started
|
||||||
|
// -> all entries, including the new one must not exceed the $limit
|
||||||
|
$limit = $hardLimit - 1;
|
||||||
|
|
||||||
|
if (count($activeEntries) > $limit) {
|
||||||
|
$i = 1;
|
||||||
|
foreach ($activeEntries as $activeEntry) {
|
||||||
|
if ($i > $limit) {
|
||||||
|
if ($hardLimit > 1) {
|
||||||
|
throw new \Exception('timesheet.start.exceeded_limit');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->stopRecording($activeEntry);
|
||||||
|
$counter++;
|
||||||
|
}
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $counter;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param TimesheetQuery $query
|
* @param TimesheetQuery $query
|
||||||
* @return QueryBuilder|Pagerfanta|array
|
* @return QueryBuilder|Pagerfanta|array
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<li class="dropdown messages-menu">
|
<li class="dropdown messages-menu">
|
||||||
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
|
<a href="#" class="dropdown-toggle ddt-large ticktac" data-toggle="dropdown">
|
||||||
<i class="{{ 'start'|icon }} fa-2x"></i>
|
<i class="{{ 'start'|icon }} fa-2x"></i>
|
||||||
<span class="label label-{% if entries|length >= kimai_context.active_warning %}danger{% else %}warning{% endif %}">{{ entries|length }}</span>
|
<span class="label label-{% if entries|length > kimai_context.active_warning %}danger{% else %}warning{% endif %}">{{ entries|length }}</span>
|
||||||
</a>
|
</a>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li class="header">
|
<li class="header">
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
|
|
||||||
public function testPostAction()
|
public function testPostAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
$data = [
|
$data = [
|
||||||
'activity' => 1,
|
'activity' => 1,
|
||||||
'project' => 1,
|
'project' => 1,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@
|
|||||||
<source>timesheet.start.error</source>
|
<source>timesheet.start.error</source>
|
||||||
<target>Zeitmessung konnte nicht gestartet werden: %reason%</target>
|
<target>Zeitmessung konnte nicht gestartet werden: %reason%</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="timesheet.start.exceeded_limit">
|
||||||
|
<source>timesheet.start.exceeded_limit</source>
|
||||||
|
<target>das Limit aktiver Zeitmessungen wurde erreicht, bitte stoppen Sie zunächst laufen Zeitmessungen</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="action.update.success">
|
<trans-unit id="action.update.success">
|
||||||
<source>action.update.success</source>
|
<source>action.update.success</source>
|
||||||
<target>Änderungen erfolgreich gespeichert</target>
|
<target>Änderungen erfolgreich gespeichert</target>
|
||||||
|
|||||||
@@ -4,19 +4,23 @@
|
|||||||
<body>
|
<body>
|
||||||
<trans-unit id="timesheet.stop.success">
|
<trans-unit id="timesheet.stop.success">
|
||||||
<source>timesheet.stop.success</source>
|
<source>timesheet.stop.success</source>
|
||||||
<target>Time-recording was stopped</target>
|
<target>Time recording was stopped</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="timesheet.stop.error">
|
<trans-unit id="timesheet.stop.error">
|
||||||
<source>timesheet.stop.error</source>
|
<source>timesheet.stop.error</source>
|
||||||
<target>Time-recording could not be stopped: %reason%</target>
|
<target>Time recording could not be stopped: %reason%</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="timesheet.start.success">
|
<trans-unit id="timesheet.start.success">
|
||||||
<source>timesheet.start.success</source>
|
<source>timesheet.start.success</source>
|
||||||
<target>Time-recording was started</target>
|
<target>Time recording was started</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="timesheet.start.error">
|
<trans-unit id="timesheet.start.error">
|
||||||
<source>timesheet.start.error</source>
|
<source>timesheet.start.error</source>
|
||||||
<target>Time-recording could not be started: %reason%</target>
|
<target>Time recording could not be started: %reason%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="timesheet.start.exceeded_limit">
|
||||||
|
<source>timesheet.start.exceeded_limit</source>
|
||||||
|
<target>the limit of active time records has been reached, please stop running time measurements first</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="action.update.success">
|
<trans-unit id="action.update.success">
|
||||||
<source>action.update.success</source>
|
<source>action.update.success</source>
|
||||||
|
|||||||
@@ -134,6 +134,26 @@ admin_lte:
|
|||||||
|
|
||||||
## Timesheets (kimai.yaml)
|
## Timesheets (kimai.yaml)
|
||||||
|
|
||||||
|
### Limit active entries
|
||||||
|
|
||||||
|
To limit the amount of active entries each user can have, the configuration `active_entries` can be changed:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
kimai:
|
||||||
|
timesheet:
|
||||||
|
active_entries:
|
||||||
|
soft_limit: 1
|
||||||
|
hard_limit: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
The `soft_limit` is used as theme setting (formerly "kimai.theme.active_warning") to display a warning if the user has at least X active recordings.
|
||||||
|
|
||||||
|
The `hard_limit` is used to detect how many active records are allowed per user (by default 3 active time-records are allowed).
|
||||||
|
If `hard_limit` is 1, the active record is automatically stopped when a new one is started.
|
||||||
|
When `hard_limit` is greater than 1 and as soon as the limit is reached, the user has to manually stop at least one active
|
||||||
|
entry (an error message is shown, indicating why it is not possible to start another one).
|
||||||
|
|
||||||
|
|
||||||
### Descriptions with Markdown
|
### Descriptions with Markdown
|
||||||
|
|
||||||
The description for every timesheet entry can be formatted in two different ways, configured with the `markdown_content` setting.
|
The description for every timesheet entry can be formatted in two different ways, configured with the `markdown_content` setting.
|
||||||
|
|||||||
@@ -29,15 +29,9 @@ Therefor your feedback is highly welcome, please post your opinion at GitHub.
|
|||||||
|
|
||||||
## Active entries warning
|
## Active entries warning
|
||||||
|
|
||||||
A small colored warning sign will be shown, if a user has more than 3 active timesheet entries.
|
A small colored warning sign will be shown, if a user has more than X active timesheet entries.
|
||||||
|
|
||||||
You can change this soft limit by setting the config key `kimai.theme.active_warning` in your `local.yaml`:
|
The amount `X` is configured in your `local.yaml` with the setting `timesheet.active_entries.soft_limit` (see [configurations.md](configurations.md)).
|
||||||
|
|
||||||
```yaml
|
|
||||||
kimai:
|
|
||||||
theme:
|
|
||||||
active_warning: 2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Colors
|
## Colors
|
||||||
|
|
||||||
@@ -56,7 +50,7 @@ Possible values are:
|
|||||||
|
|
||||||
### Fallback color
|
### Fallback color
|
||||||
|
|
||||||
Whenever a color is required but none is configured, Kimai uses a fallback language from the config key `kimai.theme.box_color`.
|
Whenever a color is required but none is configured, Kimai uses a fallback color from the config key `kimai.theme.box_color`.
|
||||||
|
|
||||||
You can change the default color `green` to any one from the above in your `local.yaml`:
|
You can change the default color `green` to any one from the above in your `local.yaml`:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user