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

@@ -61,7 +61,7 @@ abstract class TimesheetAbstractController extends AbstractController
/**
* @var TimesheetService
*/
private $service;
protected $service;
public function __construct(
UserDateTimeFactory $dateTime,

View File

@@ -9,14 +9,22 @@
namespace App\Controller;
use App\Entity\Tag;
use App\Entity\Team;
use App\Entity\Timesheet;
use App\Event\TimesheetMetaDisplayEvent;
use App\Form\Model\MultiUserTimesheet;
use App\Form\TimesheetAdminEditForm;
use App\Form\TimesheetMultiUserEditForm;
use App\Repository\ActivityRepository;
use App\Repository\ProjectRepository;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use Doctrine\Common\Collections\ArrayCollection;
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\Response;
use Symfony\Component\Routing\Annotation\Route;
@@ -59,7 +67,7 @@ class TimesheetTeamController extends TimesheetAbstractController
*
* @param Timesheet $entry
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
* @return RedirectResponse|Response
*/
public function editAction(Timesheet $entry, Request $request)
{
@@ -73,13 +81,89 @@ class TimesheetTeamController extends TimesheetAbstractController
* @param Request $request
* @param ProjectRepository $projectRepository
* @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)
{
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"})
* @Security("is_granted('edit_other_timesheet')")

View File

@@ -557,6 +557,27 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
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()
{
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
],
'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) {
return $user->getDisplayName();
},
'choice_translation_domain' => false,
]);
$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();
}
}
}