create timesheet for multiple users (#1716)
This commit is contained in:
@@ -15,6 +15,7 @@ Perform EACH version specific task between your version and the new one, otherwi
|
|||||||
### Developer
|
### Developer
|
||||||
|
|
||||||
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed
|
- **BC break**: interface method signature `HtmlToPdfConverter::convertToPdf` changed
|
||||||
|
- **BC break**: the macros `badge` and `label` do not apply the `|trans` filter any more
|
||||||
|
|
||||||
## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
|
## [1.9](https://github.com/kevinpapst/kimai2/releases/tag/1.9)
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ abstract class TimesheetAbstractController extends AbstractController
|
|||||||
/**
|
/**
|
||||||
* @var TimesheetService
|
* @var TimesheetService
|
||||||
*/
|
*/
|
||||||
private $service;
|
protected $service;
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
UserDateTimeFactory $dateTime,
|
UserDateTimeFactory $dateTime,
|
||||||
|
|||||||
@@ -9,14 +9,22 @@
|
|||||||
|
|
||||||
namespace App\Controller;
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Tag;
|
||||||
|
use App\Entity\Team;
|
||||||
use App\Entity\Timesheet;
|
use App\Entity\Timesheet;
|
||||||
use App\Event\TimesheetMetaDisplayEvent;
|
use App\Event\TimesheetMetaDisplayEvent;
|
||||||
|
use App\Form\Model\MultiUserTimesheet;
|
||||||
use App\Form\TimesheetAdminEditForm;
|
use App\Form\TimesheetAdminEditForm;
|
||||||
|
use App\Form\TimesheetMultiUserEditForm;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
use App\Repository\Query\TimesheetQuery;
|
use App\Repository\Query\TimesheetQuery;
|
||||||
use App\Repository\TagRepository;
|
use App\Repository\TagRepository;
|
||||||
|
use App\Timesheet\TrackingMode\TrackingModeInterface;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\Form\FormInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
@@ -59,7 +67,7 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
*
|
*
|
||||||
* @param Timesheet $entry
|
* @param Timesheet $entry
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function editAction(Timesheet $entry, Request $request)
|
public function editAction(Timesheet $entry, Request $request)
|
||||||
{
|
{
|
||||||
@@ -73,13 +81,89 @@ class TimesheetTeamController extends TimesheetAbstractController
|
|||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param ProjectRepository $projectRepository
|
* @param ProjectRepository $projectRepository
|
||||||
* @param ActivityRepository $activityRepository
|
* @param ActivityRepository $activityRepository
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
* @return RedirectResponse|Response
|
||||||
*/
|
*/
|
||||||
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository)
|
public function createAction(Request $request, ProjectRepository $projectRepository, ActivityRepository $activityRepository, TagRepository $tagRepository)
|
||||||
{
|
{
|
||||||
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
return $this->create($request, 'timesheet-team/edit.html.twig', $projectRepository, $activityRepository, $tagRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/create_mu", name="admin_timesheet_create_multiuser", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('create_other_timesheet')")
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return RedirectResponse|Response
|
||||||
|
*/
|
||||||
|
public function createForMultiUserAction(Request $request)
|
||||||
|
{
|
||||||
|
$entry = new MultiUserTimesheet();
|
||||||
|
$entry->setUser($this->getUser());
|
||||||
|
$this->service->prepareNewTimesheet($entry, $request);
|
||||||
|
|
||||||
|
$mode = $this->getTrackingMode();
|
||||||
|
$createForm = $this->getMultiUserCreateForm($entry, $mode);
|
||||||
|
$createForm->handleRequest($request);
|
||||||
|
|
||||||
|
if ($createForm->isSubmitted() && $createForm->isValid()) {
|
||||||
|
try {
|
||||||
|
/** @var ArrayCollection $users */
|
||||||
|
$users = $createForm->get('users')->getData();
|
||||||
|
/** @var ArrayCollection $teams */
|
||||||
|
$teams = $createForm->get('teams')->getData();
|
||||||
|
|
||||||
|
$allUsers = $users->toArray();
|
||||||
|
foreach ($teams as $team) {
|
||||||
|
$allUsers = array_merge($allUsers, $team->getUsers()->toArray());
|
||||||
|
}
|
||||||
|
$allUsers = array_unique($allUsers);
|
||||||
|
|
||||||
|
/** @var Tag[] $tags */
|
||||||
|
$tags = [];
|
||||||
|
/** @var Tag $tag */
|
||||||
|
foreach ($entry->getTags() as $tag) {
|
||||||
|
$tag->removeTimesheet($entry);
|
||||||
|
$tags[] = $tag;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($allUsers as $user) {
|
||||||
|
$newTimesheet = $entry->createCopy();
|
||||||
|
$newTimesheet->setUser($user);
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
$newTimesheet->addTag($tag);
|
||||||
|
}
|
||||||
|
$this->service->prepareNewTimesheet($newTimesheet, $request);
|
||||||
|
$this->service->saveNewTimesheet($newTimesheet);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->flashSuccess('action.update.success');
|
||||||
|
|
||||||
|
return $this->redirectToRoute($this->getTimesheetRoute());
|
||||||
|
} catch (\Exception $ex) {
|
||||||
|
$this->flashError('action.update.error', ['%reason%' => $ex->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->render('timesheet-team/edit.html.twig', [
|
||||||
|
'timesheet' => $entry,
|
||||||
|
'form' => $createForm->createView(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getMultiUserCreateForm(MultiUserTimesheet $entry, TrackingModeInterface $mode): FormInterface
|
||||||
|
{
|
||||||
|
return $this->createForm(TimesheetMultiUserEditForm::class, $entry, [
|
||||||
|
'action' => $this->generateUrl('admin_timesheet_create_multiuser'),
|
||||||
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
|
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||||
|
'include_user' => $this->includeUserInForms('create'),
|
||||||
|
'allow_begin_datetime' => $mode->canEditBegin(),
|
||||||
|
'allow_end_datetime' => $mode->canEditEnd(),
|
||||||
|
'allow_duration' => $mode->canEditDuration(),
|
||||||
|
'customer' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route(path="/multi-update", name="admin_timesheet_multi_update", methods={"POST"})
|
* @Route(path="/multi-update", name="admin_timesheet_multi_update", methods={"POST"})
|
||||||
* @Security("is_granted('edit_other_timesheet')")
|
* @Security("is_granted('edit_other_timesheet')")
|
||||||
|
|||||||
@@ -557,6 +557,27 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function createCopy(?Timesheet $timesheet = null): Timesheet
|
||||||
|
{
|
||||||
|
if (null === $timesheet) {
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
}
|
||||||
|
|
||||||
|
$values = get_object_vars($this);
|
||||||
|
foreach ($values as $k => $v) {
|
||||||
|
$timesheet->$k = $v;
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheet->meta = new ArrayCollection();
|
||||||
|
|
||||||
|
/** @var TimesheetMeta $meta */
|
||||||
|
foreach ($this->meta as $meta) {
|
||||||
|
$timesheet->setMetaField(clone $meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $timesheet;
|
||||||
|
}
|
||||||
|
|
||||||
public function __clone()
|
public function __clone()
|
||||||
{
|
{
|
||||||
if ($this->id) {
|
if ($this->id) {
|
||||||
|
|||||||
86
src/Form/Model/MultiUserTimesheet.php
Normal file
86
src/Form/Model/MultiUserTimesheet.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?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\Form\Model;
|
||||||
|
|
||||||
|
use App\Entity\Team;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @App\Validator\Constraints\TimesheetMultiUser
|
||||||
|
*/
|
||||||
|
final class MultiUserTimesheet extends Timesheet
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var Collection<User>
|
||||||
|
*/
|
||||||
|
private $users;
|
||||||
|
/**
|
||||||
|
* @var Collection<Team>
|
||||||
|
*/
|
||||||
|
private $teams;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
parent::__construct();
|
||||||
|
$this->users = new ArrayCollection();
|
||||||
|
$this->teams = new ArrayCollection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<User>
|
||||||
|
*/
|
||||||
|
public function getUsers(): Collection
|
||||||
|
{
|
||||||
|
return $this->users;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addUser(User $user)
|
||||||
|
{
|
||||||
|
$this->users->add($user);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeUser(User $user)
|
||||||
|
{
|
||||||
|
if ($this->users->contains($user)) {
|
||||||
|
$this->users->remove($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<Team>
|
||||||
|
*/
|
||||||
|
public function getTeams(): Collection
|
||||||
|
{
|
||||||
|
return $this->teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function addTeam(Team $team)
|
||||||
|
{
|
||||||
|
$this->teams->add($team);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeTeam(Team $team)
|
||||||
|
{
|
||||||
|
if ($this->teams->contains($team)) {
|
||||||
|
$this->teams->remove($team);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Form/TimesheetMultiUserEditForm.php
Normal file
43
src/Form/TimesheetMultiUserEditForm.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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\Form;
|
||||||
|
|
||||||
|
use App\Form\Type\TeamMemberType;
|
||||||
|
use App\Form\Type\TeamType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class TimesheetMultiUserEditForm extends TimesheetAdminEditForm
|
||||||
|
{
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$options['allow_begin_datetime'] = true;
|
||||||
|
$options['allow_end_datetime'] = true;
|
||||||
|
$options['allow_duration'] = false;
|
||||||
|
$options['include_user'] = false;
|
||||||
|
|
||||||
|
parent::buildForm($builder, $options);
|
||||||
|
|
||||||
|
$builder->add('users', TeamMemberType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('teams', TeamType::class, [
|
||||||
|
'multiple' => true,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
parent::configureOptions($resolver);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,6 +37,7 @@ class PageSizeType extends AbstractType
|
|||||||
500 => 500
|
500 => 500
|
||||||
],
|
],
|
||||||
'placeholder' => null,
|
'placeholder' => null,
|
||||||
|
'choice_translation_domain' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
68
src/Form/Type/TeamMemberType.php
Normal file
68
src/Form/Type/TeamMemberType.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?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\Form\Type;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\OptionsResolver\Options;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select a user that
|
||||||
|
*/
|
||||||
|
class TeamMemberType extends AbstractType
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'class' => User::class,
|
||||||
|
'label' => 'label.user',
|
||||||
|
'choice_label' => function (User $user) {
|
||||||
|
return $user->getDisplayName();
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
|
return function (UserRepository $repo) use ($options) {
|
||||||
|
$qb = $repo->createQueryBuilder('u');
|
||||||
|
$qb
|
||||||
|
->andWhere($qb->expr()->eq('u.enabled', ':enabled'))
|
||||||
|
->setParameter('enabled', true, \PDO::PARAM_BOOL)
|
||||||
|
->orderBy('u.username', 'ASC');
|
||||||
|
|
||||||
|
/** @var User $user */
|
||||||
|
$user = $options['user'];
|
||||||
|
|
||||||
|
if (null !== $user && !$user->getTeams()->isEmpty() && !$user->isSuperAdmin() && !$user->isAdmin()) {
|
||||||
|
$qb
|
||||||
|
->leftJoin('u.teams', 'teams')
|
||||||
|
->leftJoin('teams.users', 'users')
|
||||||
|
->andWhere($qb->expr()->isMemberOf(':teams', 'u.teams'))
|
||||||
|
->setParameter('teams', $user->getTeams());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $qb;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function getParent()
|
||||||
|
{
|
||||||
|
return EntityType::class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ class UserType extends AbstractType
|
|||||||
'choice_label' => function (User $user) {
|
'choice_label' => function (User $user) {
|
||||||
return $user->getDisplayName();
|
return $user->getDisplayName();
|
||||||
},
|
},
|
||||||
|
'choice_translation_domain' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$resolver->setDefault('query_builder', function (Options $options) {
|
$resolver->setDefault('query_builder', function (Options $options) {
|
||||||
|
|||||||
33
src/Validator/Constraints/TimesheetMultiUser.php
Normal file
33
src/Validator/Constraints/TimesheetMultiUser.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?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\Validator\Constraints;
|
||||||
|
|
||||||
|
use Doctrine\Common\Annotations\Annotation\Target;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Annotation
|
||||||
|
* @Target({"CLASS", "PROPERTY", "METHOD", "ANNOTATION"})
|
||||||
|
*/
|
||||||
|
class TimesheetMultiUser extends Constraint
|
||||||
|
{
|
||||||
|
public const MISSING_USER_OR_TEAM = 'ts-multi-user-01';
|
||||||
|
|
||||||
|
protected static $errorNames = [
|
||||||
|
self::MISSING_USER_OR_TEAM => 'You must select at least one user or team.',
|
||||||
|
];
|
||||||
|
|
||||||
|
public $message = 'This form has invalid settings.';
|
||||||
|
|
||||||
|
public function getTargets()
|
||||||
|
{
|
||||||
|
return self::CLASS_CONSTRAINT;
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/Validator/Constraints/TimesheetMultiUserValidator.php
Normal file
47
src/Validator/Constraints/TimesheetMultiUserValidator.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?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\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Form\Model\MultiUserTimesheet;
|
||||||
|
use Symfony\Component\Validator\Constraint;
|
||||||
|
use Symfony\Component\Validator\ConstraintValidator;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
|
||||||
|
final class TimesheetMultiUserValidator extends ConstraintValidator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Timesheet|mixed $value
|
||||||
|
* @param Constraint $constraint
|
||||||
|
*/
|
||||||
|
public function validate($value, Constraint $constraint)
|
||||||
|
{
|
||||||
|
if (!($constraint instanceof TimesheetMultiUser)) {
|
||||||
|
throw new UnexpectedTypeException($constraint, TimesheetMultiUser::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!\is_object($value) || !($value instanceof MultiUserTimesheet)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($value->getTeams()->isEmpty() && $value->getUsers()->isEmpty()) {
|
||||||
|
$this->context->buildViolation('You must select at least one user or team.')
|
||||||
|
->atPath('users')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM)
|
||||||
|
->addViolation();
|
||||||
|
|
||||||
|
$this->context->buildViolation('You must select at least one user or team.')
|
||||||
|
->atPath('teams')
|
||||||
|
->setTranslationDomain('validators')
|
||||||
|
->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM)
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -37,9 +37,9 @@
|
|||||||
{% macro label_boolean(visible) %}
|
{% macro label_boolean(visible) %}
|
||||||
{% import _self as macro %}
|
{% import _self as macro %}
|
||||||
{% if visible %}
|
{% if visible %}
|
||||||
{{ macro.label('yes', 'success') }}
|
{{ macro.label('yes'|trans, 'success') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ macro.label('no', 'default') }}
|
{{ macro.label('no'|trans, 'default') }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
@@ -50,17 +50,17 @@
|
|||||||
|
|
||||||
{% macro label_role(role) %}
|
{% macro label_role(role) %}
|
||||||
{% import _self as macro %}
|
{% import _self as macro %}
|
||||||
|
{% set color = 'primary' %}
|
||||||
{% if role == 'ROLE_SUPER_ADMIN' %}
|
{% if role == 'ROLE_SUPER_ADMIN' %}
|
||||||
{{ macro.label(role, 'danger') }}
|
{% set color = 'danger' %}
|
||||||
{% elseif role == 'ROLE_ADMIN' %}
|
{% elseif role == 'ROLE_ADMIN' %}
|
||||||
{{ macro.label(role, 'warning') }}
|
{% set color = 'warning' %}
|
||||||
{% elseif role == 'ROLE_TEAMLEAD' %}
|
{% elseif role == 'ROLE_TEAMLEAD' %}
|
||||||
{{ macro.label(role, 'success') }}
|
{% set color = 'success' %}
|
||||||
{% elseif role == 'ROLE_USER' %}
|
{% elseif role == 'ROLE_USER' %}
|
||||||
{{ macro.label(role, 'gray') }}
|
{% set color = 'gray' %}
|
||||||
{% else %}
|
|
||||||
{{ macro.label(role, 'primary') }}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ macro.label(role|trans, color) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro username(user) %}
|
{% macro username(user) %}
|
||||||
@@ -155,11 +155,11 @@
|
|||||||
|
|
||||||
{% macro label(title, type, tooltip) %}
|
{% macro label(title, type, tooltip) %}
|
||||||
{# success, warning, danger, primary #}
|
{# success, warning, danger, primary #}
|
||||||
<span {% if tooltip %}data-toggle="tooltip" data-placement="top" title="{{ tooltip }}" {% endif %}class="label label-{{ type|default('success') }}">{{ title|trans }}</span>
|
<span {% if tooltip %}data-toggle="tooltip" data-placement="top" title="{{ tooltip }}" {% endif %}class="label label-{{ type|default('success') }}">{{ title }}</span>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro badge(title, color) %}
|
{% macro badge(title, color) %}
|
||||||
<span class="badge" style="background-color:{{ color }}">{{ title|trans }}</span>
|
<span class="badge" style="background-color:{{ color }}">{{ title }}</span>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro alert(type, description, title, icon) %}
|
{% macro alert(type, description, title, icon) %}
|
||||||
@@ -345,7 +345,7 @@
|
|||||||
<span class="caret"></span>
|
<span class="caret"></span>
|
||||||
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
|
<span class="sr-only">{{ 'label.toggle_dropdown'|trans }}</span>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu" role="menu">
|
<ul class="dropdown-menu dropdown-menu-right" role="menu">
|
||||||
{% for childIcon,childValues in values.children %}
|
{% for childIcon,childValues in values.children %}
|
||||||
<li>{{ macro.action_button(childIcon, childValues, false) }}</li>
|
<li>{{ macro.action_button(childIcon, childValues, false) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
|
{% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
|
||||||
{% if is_granted('create_other_timesheet') %}
|
{% if is_granted('create_other_timesheet') %}
|
||||||
{% set actions = actions|merge({'create': {'url': path('admin_timesheet_create'), 'class': 'modal-ajax-form'}}) %}
|
{% set actions = actions|merge({'create': {'children': {'single': {'title': 'create'|trans,'url': path('admin_timesheet_create'), 'class': 'create-ts modal-ajax-form'}, 'multi-user': {'title': 'create-timesheet-multiuser'|trans({}, 'actions'),'url': path('admin_timesheet_create_multiuser'), 'class': 'create-ts-mu modal-ajax-form'}}}}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% set actions = actions|merge({'help': {'url': 'timesheet.html'|docu_link, 'target': '_blank'}}) %}
|
{% set actions = actions|merge({'help': {'url': 'timesheet.html'|docu_link, 'target': '_blank'}}) %}
|
||||||
|
|||||||
@@ -57,6 +57,27 @@
|
|||||||
{% if form.tags is defined %}
|
{% if form.tags is defined %}
|
||||||
{{ form_row(form.tags) }}
|
{{ form_row(form.tags) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if form.user is defined %}
|
||||||
|
{{ form_row(form.user) }}
|
||||||
|
{% endif %}
|
||||||
|
{% if form.users is defined or form.teams is defined %}
|
||||||
|
{% set uLength = 12 %}
|
||||||
|
{% if form.users is defined and form.teams is defined %}
|
||||||
|
{% set uLength = 6 %}
|
||||||
|
{% endif %}
|
||||||
|
<div class="row">
|
||||||
|
{% if form.users is defined %}
|
||||||
|
<div class="col-md-{{ uLength }}">
|
||||||
|
{{ form_row(form.users) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if form.teams is defined %}
|
||||||
|
<div class="col-md-{{ uLength }}">
|
||||||
|
{{ form_row(form.teams) }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if form.fixedRate is defined and form.hourlyRate is defined %}
|
{% if form.fixedRate is defined and form.hourlyRate is defined %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
|
|||||||
@@ -47,7 +47,8 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'),
|
'toolbar-action exporter-pdf' => $this->createUrl('/team/timesheet/export/pdf'),
|
||||||
'toolbar-action exporter-xlsx' => $this->createUrl('/team/timesheet/export/xlsx'),
|
'toolbar-action exporter-xlsx' => $this->createUrl('/team/timesheet/export/xlsx'),
|
||||||
'visibility' => '#',
|
'visibility' => '#',
|
||||||
'create modal-ajax-form' => $this->createUrl('/team/timesheet/create'),
|
'create-ts modal-ajax-form' => $this->createUrl('/team/timesheet/create'),
|
||||||
|
'create-ts-mu modal-ajax-form' => $this->createUrl('/team/timesheet/create_mu'),
|
||||||
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
'help' => 'https://www.kimai.org/documentation/timesheet.html'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -200,6 +201,65 @@ class TimesheetTeamControllerTest extends ControllerBaseTest
|
|||||||
$this->assertNull($timesheet->getFixedRate());
|
$this->assertNull($timesheet->getFixedRate());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testCreateForMultipleUsersAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$this->request($client, '/team/timesheet/create_mu');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('form[name=timesheet_multi_user_edit_form]')->form();
|
||||||
|
$client->submit($form, [
|
||||||
|
'timesheet_multi_user_edit_form' => [
|
||||||
|
'description' => 'Testing is more fun!',
|
||||||
|
'project' => 1,
|
||||||
|
'activity' => 1,
|
||||||
|
'teams' => '1',
|
||||||
|
'tags' => 'test,1234,foo-bar',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertIsRedirect($client, $this->createUrl('/team/timesheet/'));
|
||||||
|
$client->followRedirect();
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
$this->assertHasFlashSuccess($client);
|
||||||
|
|
||||||
|
$em = $this->getEntityManager();
|
||||||
|
/** @var Timesheet[] $timesheets */
|
||||||
|
$timesheets = $em->getRepository(Timesheet::class)->findAll();
|
||||||
|
$this->assertCount(2, $timesheets);
|
||||||
|
foreach ($timesheets as $timesheet) {
|
||||||
|
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
|
||||||
|
$this->assertNull($timesheet->getEnd());
|
||||||
|
$this->assertEquals('Testing is more fun!', $timesheet->getDescription());
|
||||||
|
$this->assertEquals(0, $timesheet->getRate());
|
||||||
|
$this->assertNull($timesheet->getHourlyRate());
|
||||||
|
$this->assertNull($timesheet->getFixedRate());
|
||||||
|
$this->assertEquals(['test', '1234', 'foo-bar'], $timesheet->getTagsAsArray());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testCreateForMultipleUsersActionWithoutUserOrTeam()
|
||||||
|
{
|
||||||
|
$data = [
|
||||||
|
'timesheet_multi_user_edit_form' => [
|
||||||
|
'description' => 'Testing is more fun!',
|
||||||
|
'project' => 1,
|
||||||
|
'activity' => 1,
|
||||||
|
// make sure the default validation for timesheets is applied as well
|
||||||
|
'begin' => (new \DateTime())->format('Y-m-d H:i'),
|
||||||
|
'end' => (new \DateTime('-1 hour'))->format('Y-m-d H:i'),
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->assertFormHasValidationError(
|
||||||
|
User::ROLE_ADMIN,
|
||||||
|
'/team/timesheet/create_mu',
|
||||||
|
'form[name=timesheet_multi_user_edit_form]',
|
||||||
|
$data,
|
||||||
|
['#timesheet_multi_user_edit_form_users', '#timesheet_multi_user_edit_form_teams', '#timesheet_multi_user_edit_form_end']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public function testEditAction()
|
public function testEditAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?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\Validator\Constraints;
|
||||||
|
|
||||||
|
use App\Form\Model\MultiUserTimesheet;
|
||||||
|
use App\Validator\Constraints\TimesheetMultiUser;
|
||||||
|
use App\Validator\Constraints\TimesheetMultiUserValidator;
|
||||||
|
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||||
|
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||||
|
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Validator\Constraints\TimesheetMultiUserValidator
|
||||||
|
*/
|
||||||
|
class TimesheetMultiUserValidatorTest extends ConstraintValidatorTestCase
|
||||||
|
{
|
||||||
|
protected function createValidator($isGranted = true)
|
||||||
|
{
|
||||||
|
return new TimesheetMultiUserValidator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstraintIsInvalid()
|
||||||
|
{
|
||||||
|
$this->expectException(UnexpectedTypeException::class);
|
||||||
|
|
||||||
|
$this->validator->validate('foo', new NotBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyTimesheet()
|
||||||
|
{
|
||||||
|
$timesheet = new MultiUserTimesheet();
|
||||||
|
|
||||||
|
$this->validator->validate($timesheet, new TimesheetMultiUser(['message' => 'myMessage']));
|
||||||
|
|
||||||
|
$this->buildViolation('You must select at least one user or team.')
|
||||||
|
->atPath('property.path.users')
|
||||||
|
->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM)
|
||||||
|
->buildNextViolation('You must select at least one user or team.')
|
||||||
|
->atPath('property.path.teams')
|
||||||
|
->setCode(TimesheetMultiUser::MISSING_USER_OR_TEAM)
|
||||||
|
->assertRaised();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -70,6 +70,10 @@
|
|||||||
<source>create-timesheet</source>
|
<source>create-timesheet</source>
|
||||||
<target>Zeit erfassen</target>
|
<target>Zeit erfassen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="create-timesheet-multiuser">
|
||||||
|
<source>create-timesheet-multiuser</source>
|
||||||
|
<target>Für mehrere Benutzer</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="permissions">
|
<trans-unit id="permissions">
|
||||||
<source>permissions</source>
|
<source>permissions</source>
|
||||||
<target>Team Berechtigungen</target>
|
<target>Team Berechtigungen</target>
|
||||||
|
|||||||
@@ -70,6 +70,10 @@
|
|||||||
<source>create-timesheet</source>
|
<source>create-timesheet</source>
|
||||||
<target>Create timesheet</target>
|
<target>Create timesheet</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="create-timesheet-multiuser">
|
||||||
|
<source>create-timesheet-multiuser</source>
|
||||||
|
<target>For multiple users</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="permissions">
|
<trans-unit id="permissions">
|
||||||
<source>permissions</source>
|
<source>permissions</source>
|
||||||
<target>Team permissions</target>
|
<target>Team permissions</target>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
<source>The begin date cannot be in the future.</source>
|
<source>The begin date cannot be in the future.</source>
|
||||||
<target>Das Startdatum darf nicht in der Zukunft liegen.</target>
|
<target>Das Startdatum darf nicht in der Zukunft liegen.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="You must select at least one user or team.">
|
||||||
|
<source>You must select at least one user or team.</source>
|
||||||
|
<target>Sie müssen mindestens einen Benutzer oder ein Team auswählen.</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
<source>The begin date cannot be in the future.</source>
|
<source>The begin date cannot be in the future.</source>
|
||||||
<target>The begin date cannot be in the future.</target>
|
<target>The begin date cannot be in the future.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="You must select at least one user or team.">
|
||||||
|
<source>You must select at least one user or team.</source>
|
||||||
|
<target>You must select at least one user or team.</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
Reference in New Issue
Block a user