create timesheet for multiple users (#1716)

This commit is contained in:
Kevin Papst
2020-05-18 23:35:43 +02:00
committed by GitHub
parent 9da46bbdcd
commit e61ffe4db4
20 changed files with 548 additions and 16 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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')")

View File

@@ -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) {

View 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;
}
}

View 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);
}
}

View File

@@ -37,6 +37,7 @@ class PageSizeType extends AbstractType
500 => 500 500 => 500
], ],
'placeholder' => null, 'placeholder' => null,
'choice_translation_domain' => false,
]); ]);
} }

View 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;
}
}

View File

@@ -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) {

View 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;
}
}

View 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();
}
}
}

View File

@@ -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 %}

View File

@@ -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'}}) %}

View File

@@ -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">

View File

@@ -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);

View File

@@ -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();
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>