added tags for timesheets (#604)
This commit is contained in:
115
src/API/TagController.php
Normal file
115
src/API/TagController.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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\API;
|
||||
|
||||
use App\Repository\TagRepository;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Swagger\Annotations as SWG;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* @RouteResource("Tag")
|
||||
*/
|
||||
class TagController extends BaseApiController
|
||||
{
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $repository;
|
||||
|
||||
/**
|
||||
* @var ViewHandlerInterface
|
||||
*/
|
||||
protected $viewHandler;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TagRepository $repository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TagRepository $repository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @SWG\Response(
|
||||
* response=200,
|
||||
* description="Returns the collection of all existing tags as string array",
|
||||
* @SWG\Schema(
|
||||
* type="array",
|
||||
* @SWG\Items(type="string")
|
||||
* )
|
||||
* )
|
||||
*
|
||||
* @Rest\QueryParam(name="name", requirements="[a-zA-Z0-9 -\.]+", strict=true, nullable=true, description="Search term to filter tag list")
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function cgetAction(ParamFetcherInterface $paramFetcher)
|
||||
{
|
||||
$filter = $paramFetcher->get('name');
|
||||
|
||||
$data = $this->repository->findAllTagNames($filter);
|
||||
if (null === $data) {
|
||||
$data = [];
|
||||
}
|
||||
$view = new View($data, 200);
|
||||
$view->getContext()->setGroups(['Default', 'Collection']);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing tag
|
||||
*
|
||||
* @SWG\Delete(
|
||||
* @SWG\Response(
|
||||
* response=204,
|
||||
* description="Delete one tag"
|
||||
* ),
|
||||
* )
|
||||
* @SWG\Parameter(
|
||||
* name="id",
|
||||
* in="path",
|
||||
* type="integer",
|
||||
* description="Tag ID to delete",
|
||||
* required=true,
|
||||
* )
|
||||
*
|
||||
* @Security("is_granted('delete_tag')")
|
||||
*
|
||||
* @param int $id
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction($id)
|
||||
{
|
||||
$tag = $this->repository->find($id);
|
||||
|
||||
if (null === $tag) {
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
$entityManager = $this->getDoctrine()->getManager();
|
||||
$entityManager->remove($tag);
|
||||
$entityManager->flush();
|
||||
|
||||
$view = new View(null, Response::HTTP_NO_CONTENT);
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,10 @@ use App\Configuration\TimesheetConfiguration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\UserDateTimeFactory;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use FOS\RestBundle\Controller\Annotations as Rest;
|
||||
use FOS\RestBundle\Controller\Annotations\RouteResource;
|
||||
use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
@@ -49,24 +51,29 @@ class TimesheetController extends BaseApiController
|
||||
* @var TimesheetConfiguration
|
||||
*/
|
||||
protected $configuration;
|
||||
|
||||
/**
|
||||
* @var UserDateTimeFactory
|
||||
*/
|
||||
protected $dateTime;
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
protected $tagRepository;
|
||||
|
||||
/**
|
||||
* @param ViewHandlerInterface $viewHandler
|
||||
* @param TimesheetRepository $repository
|
||||
* @param UserDateTimeFactory $dateTime
|
||||
* @param TimesheetConfiguration $configuration
|
||||
* @param TagRepository $tagRepository
|
||||
*/
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, TimesheetRepository $repository, UserDateTimeFactory $dateTime, TimesheetConfiguration $configuration, TagRepository $tagRepository)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
$this->dateTime = $dateTime;
|
||||
$this->tagRepository = $tagRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -87,6 +94,7 @@ class TimesheetController extends BaseApiController
|
||||
* @Rest\QueryParam(name="activity", requirements="\d+", strict=true, nullable=true, description="Activity ID to filter timesheets")
|
||||
* @Rest\QueryParam(name="page", requirements="\d+", strict=true, nullable=true, description="The page to display, renders a 404 if not found (default: 1)")
|
||||
* @Rest\QueryParam(name="size", requirements="\d+", strict=true, nullable=true, description="The amount of entries for each page (default: 25)")
|
||||
* @Rest\QueryParam(name="tags", requirements="[a-zA-Z0-9 -,]+", strict=true, nullable=true, description="The name of tags which are in the datasets")
|
||||
* @Rest\QueryParam(name="orderBy", requirements="id|begin|end|rate", strict=true, nullable=true, description="The field by which results will be ordered. Allowed values: id, begin, end, rate (default: begin)")
|
||||
* @Rest\QueryParam(name="order", requirements="ASC|DESC", strict=true, nullable=true, description="The result order. Allowed values: ASC, DESC (default: DESC)")
|
||||
* @Rest\QueryParam(name="begin", requirements=@Constraints\DateTime, strict=true, nullable=true, description="Only records after this date will be included (format: ISO 8601)")
|
||||
@@ -133,6 +141,13 @@ class TimesheetController extends BaseApiController
|
||||
$query->setPageSize($size);
|
||||
}
|
||||
|
||||
if (null !== ($tags = $paramFetcher->get('tags'))) {
|
||||
$ids = $this->tagRepository->findIdsByTagNameList($tags);
|
||||
if ($ids !== null && sizeof($ids) > 0) {
|
||||
$query->setTags(new ArrayCollection($ids));
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== ($order = $paramFetcher->get('order'))) {
|
||||
$query->setOrder($order);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Calendar;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
|
||||
class TimesheetEntity
|
||||
@@ -45,6 +46,10 @@ class TimesheetEntity
|
||||
* @var string
|
||||
*/
|
||||
protected $activity;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $tags;
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
@@ -66,6 +71,14 @@ class TimesheetEntity
|
||||
$this->customer = $entry->getProject()->getCustomer()->getName();
|
||||
$this->project = $entry->getProject()->getName();
|
||||
$this->activity = $entry->getActivity()->getName();
|
||||
if (sizeof($entry->getTags()) > 0) {
|
||||
$arr = [];
|
||||
/** @var Tag $tag */
|
||||
foreach ($entry->getTags() as $tag) {
|
||||
array_push($arr, $tag->getName());
|
||||
}
|
||||
$this->tags = implode(', ', $arr);
|
||||
}
|
||||
|
||||
$color = $entry->getActivity()->getColor();
|
||||
if (empty($color)) {
|
||||
@@ -235,6 +248,25 @@ class TimesheetEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getTags(): ?string
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $tags
|
||||
* @return TimesheetEntity
|
||||
*/
|
||||
public function setTags(?string $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null|string
|
||||
*/
|
||||
|
||||
@@ -345,7 +345,7 @@ class InvoiceController extends AbstractController
|
||||
'method' => 'POST',
|
||||
'attr' => [
|
||||
'id' => 'invoice-print-form'
|
||||
]
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
70
src/Controller/TagController.php
Normal file
70
src/Controller/TagController.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Form\Toolbar\TagToolbarForm;
|
||||
use App\Repository\Query\TagQuery;
|
||||
use App\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* @Route(path="/admin/tags")
|
||||
* @Security("is_granted('view_tag')")
|
||||
*/
|
||||
class TagController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/", defaults={"page": 1}, name="tags", methods={"GET"})
|
||||
* @Route(path="/page/{page}", requirements={"page": "[1-9]\d*"}, name="tags_paginated", methods={"GET"})
|
||||
*
|
||||
* @param TagRepository $repository
|
||||
* @param Request $request
|
||||
* @param int $page
|
||||
* @return Response
|
||||
*/
|
||||
public function listTags(TagRepository $repository, Request $request, $page)
|
||||
{
|
||||
$query = new TagQuery();
|
||||
$query->setPage($page);
|
||||
|
||||
$form = $this->getToolbarForm($query);
|
||||
$form->handleRequest($request);
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var TagQuery $query */
|
||||
$query = $form->getData();
|
||||
}
|
||||
|
||||
$tags = $repository->getTagCount($query);
|
||||
|
||||
return $this->render('tags/index.html.twig', [
|
||||
'tags' => $tags,
|
||||
'query' => $query,
|
||||
'showFilter' => $form->isSubmitted(),
|
||||
'toolbarForm' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TagQuery $query
|
||||
* @return \Symfony\Component\Form\FormInterface
|
||||
*/
|
||||
protected function getToolbarForm(TagQuery $query)
|
||||
{
|
||||
return $this->createForm(TagToolbarForm::class, $query, [
|
||||
'action' => $this->generateUrl('tags', [
|
||||
'page' => $query->getPage(),
|
||||
]),
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,14 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -61,6 +63,14 @@ class TimesheetController extends AbstractController
|
||||
|
||||
$query->setUser($this->getUser());
|
||||
|
||||
if ($query->hasTags()) {
|
||||
$query->setTags(
|
||||
new ArrayCollection(
|
||||
$this->getDoctrine()->getRepository(Tag::class)->findIdsByTagNameList(implode(',', $query->getTags()->toArray()))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
@@ -149,8 +159,7 @@ class TimesheetController extends AbstractController
|
||||
->setBegin($this->dateTime->createDateTime())
|
||||
->setUser($user)
|
||||
->setActivity($timesheet->getActivity())
|
||||
->setProject($timesheet->getProject())
|
||||
;
|
||||
->setProject($timesheet->getProject());
|
||||
|
||||
$errors = $validator->validate($entry);
|
||||
|
||||
@@ -213,7 +222,7 @@ class TimesheetController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", name="timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
|
||||
@@ -9,13 +9,14 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Form\DeleteType;
|
||||
use App\Form\TimesheetEditForm;
|
||||
use App\Form\Toolbar\TimesheetToolbarForm;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\Query\TimesheetQuery;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
@@ -59,6 +60,14 @@ class TimesheetTeamController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
if ($query->hasTags()) {
|
||||
$query->setTags(
|
||||
new ArrayCollection(
|
||||
$this->getDoctrine()->getRepository(Tag::class)->findIdsByTagNameList(implode(',', $query->getTags()->toArray()))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* @var $entries Pagerfanta */
|
||||
$entries = $this->getRepository()->findByQuery($query);
|
||||
|
||||
@@ -137,7 +146,7 @@ class TimesheetTeamController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route(path="/{id}/delete", defaults={"page": 1}, name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Route(path="/{id}/delete", name="admin_timesheet_delete", methods={"GET", "POST"})
|
||||
* @Security("is_granted('delete', entry)")
|
||||
*
|
||||
* @param Timesheet $entry
|
||||
|
||||
86
src/DataFixtures/TagFixtures.php
Normal file
86
src/DataFixtures/TagFixtures.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\DataFixtures;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use Doctrine\Bundle\FixturesBundle\Fixture;
|
||||
use Doctrine\Common\Persistence\ObjectManager;
|
||||
use Faker\Factory;
|
||||
|
||||
/**
|
||||
* Defines the sample data to load in the database when running the unit and
|
||||
* functional tests or while development.
|
||||
*
|
||||
* Execute this command to load the data:
|
||||
* bin/console doctrine:fixtures:load
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class TagFixtures extends Fixture
|
||||
{
|
||||
public const MIN_TAGS = 50;
|
||||
public const MAX_TAGS = 2000;
|
||||
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function load(ObjectManager $manager)
|
||||
{
|
||||
$faker = Factory::create();
|
||||
$amount = rand(self::MIN_TAGS, self::MAX_TAGS);
|
||||
$existing = [];
|
||||
|
||||
for ($i = 0; $i < $amount; $i++) {
|
||||
$tag = new Tag();
|
||||
|
||||
$tagName = null;
|
||||
if ($i % 2 == 9) {
|
||||
$tagName = $faker->companyEmail;
|
||||
} elseif ($i % 2 == 8) {
|
||||
$tagName = $faker->firstName;
|
||||
} elseif ($i % 2 == 7) {
|
||||
$tagName = $faker->lastName;
|
||||
} elseif ($i % 6 == 0) {
|
||||
$tagName = $faker->iban();
|
||||
} elseif ($i % 5 == 0) {
|
||||
$tagName = $faker->city;
|
||||
} elseif ($i % 4 == 0) {
|
||||
$tagName = $faker->word;
|
||||
} elseif ($i % 3 == 0) {
|
||||
$tagName = $faker->streetName;
|
||||
} elseif ($i % 2 == 0) {
|
||||
$tagName = $faker->colorName;
|
||||
} elseif ($i % 1 == 0) {
|
||||
$tagName = $faker->text(rand(10, 20));
|
||||
}
|
||||
|
||||
if (in_array($tagName, $existing)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing[] = $tagName;
|
||||
$tag->setName($tagName);
|
||||
|
||||
$manager->persist($tag);
|
||||
|
||||
if ($i % self::BATCH_SIZE == 0) {
|
||||
$manager->flush();
|
||||
$manager->clear(Tag::class);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear(Tag::class);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ namespace App\DataFixtures;
|
||||
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
@@ -50,6 +51,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
return [
|
||||
UserFixtures::class,
|
||||
CustomerFixtures::class,
|
||||
TagFixtures::class,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -61,6 +63,7 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$allUser = $this->getAllUsers($manager);
|
||||
$activities = $this->getAllActivities($manager);
|
||||
$projects = $this->getAllProjects($manager);
|
||||
$allTags = $this->getAllTags($manager);
|
||||
|
||||
$faker = Factory::create();
|
||||
|
||||
@@ -86,13 +89,13 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$user,
|
||||
$activities[array_rand($activities)],
|
||||
$projects[array_rand($projects)],
|
||||
$description
|
||||
$description,
|
||||
true
|
||||
);
|
||||
|
||||
$manager->persist($entry);
|
||||
|
||||
if ($i % self::BATCH_SIZE == 0) {
|
||||
//echo '['.$i.'] Timesheets for User ' . $user->getId() . PHP_EOL;
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
@@ -115,6 +118,37 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
$manager->clear(Timesheet::class);
|
||||
}
|
||||
$manager->flush();
|
||||
|
||||
$entries = $manager->getRepository(Timesheet::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$tagAmount = rand(0, 4);
|
||||
for ($iTag = 0; $iTag < $tagAmount; $iTag++) {
|
||||
$tagId = rand(1, TagFixtures::MAX_TAGS);
|
||||
if (isset($allTags[$tagId])) {
|
||||
$temp->addTag($allTags[$tagId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear(Timesheet::class);
|
||||
$manager->clear(Tag::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ObjectManager $manager
|
||||
* @return Tag[]
|
||||
*/
|
||||
protected function getAllTags(ObjectManager $manager)
|
||||
{
|
||||
$all = [];
|
||||
/* @var Tag[] $entries */
|
||||
$entries = $manager->getRepository(Tag::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$all[$temp->getId()] = $temp;
|
||||
}
|
||||
|
||||
return $all;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
118
src/Entity/Tag.php
Normal file
118
src/Entity/Tag.php
Normal file
@@ -0,0 +1,118 @@
|
||||
<?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\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="kimai2_tags",
|
||||
* uniqueConstraints={
|
||||
* @ORM\UniqueConstraint(columns={"name"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\Entity(repositoryClass="App\Repository\TagRepository")
|
||||
* @UniqueEntity("name")
|
||||
*/
|
||||
class Tag
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="IDENTITY")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(name="name", type="string", length=255, nullable=false)
|
||||
* @Assert\NotBlank()
|
||||
* @Assert\Length(min=2, max=255)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var Timesheet[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Timesheet", mappedBy="tags", fetch="EXTRA_LAZY")
|
||||
*/
|
||||
protected $timesheets;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timesheets = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $tagName
|
||||
* @return Tag
|
||||
*/
|
||||
public function setName($tagName)
|
||||
{
|
||||
$this->name = $tagName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
*/
|
||||
public function addTimesheet(Timesheet $timesheet)
|
||||
{
|
||||
if ($this->timesheets->contains($timesheet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->timesheets->add($timesheet);
|
||||
$timesheet->addTag($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Timesheet $timesheet
|
||||
*/
|
||||
public function removeTimesheet(Timesheet $timesheet)
|
||||
{
|
||||
if (!$this->timesheets->contains($timesheet)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->timesheets->removeElement($timesheet);
|
||||
$timesheet->removeTag($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function __toString()
|
||||
{
|
||||
return $this->getName();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@@ -122,6 +123,30 @@ class Timesheet
|
||||
*/
|
||||
private $exported = false;
|
||||
|
||||
/**
|
||||
* @var \App\Entity\Tag[]
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Tag", inversedBy="timesheets", cascade={"persist"})
|
||||
* @ORM\JoinTable(
|
||||
* name="kimai2_timesheet_tags",
|
||||
* joinColumns={
|
||||
* @ORM\JoinColumn(name="timesheet_id", referencedColumnName="id")
|
||||
* },
|
||||
* inverseJoinColumns={
|
||||
* @ORM\JoinColumn(name="tag_id", referencedColumnName="id")
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
/**
|
||||
* Default constructor, initializes collections
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get entry id
|
||||
*
|
||||
@@ -319,6 +344,54 @@ class Timesheet
|
||||
return $this->rate;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
* @return Timesheet
|
||||
*/
|
||||
public function addTag(Tag $tag)
|
||||
{
|
||||
if ($this->tags->contains($tag)) {
|
||||
return $this;
|
||||
}
|
||||
$this->tags->add($tag);
|
||||
$tag->addTimesheet($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Tag $tag
|
||||
*/
|
||||
public function removeTag(Tag $tag)
|
||||
{
|
||||
if (!$this->tags->contains($tag)) {
|
||||
return;
|
||||
}
|
||||
$this->tags->removeElement($tag);
|
||||
$tag->removeTimesheet($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Tag[]|ArrayCollection
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTagsAsArray()
|
||||
{
|
||||
return array_map(
|
||||
function (Tag $element) {
|
||||
return $element->getName();
|
||||
},
|
||||
$this->getTags()->toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
||||
@@ -116,6 +116,12 @@ class MenuSubscriber implements EventSubscriberInterface
|
||||
new MenuItemModel('activity_admin', 'menu.admin_activity', 'admin_activity', [], 'fas fa-tasks')
|
||||
);
|
||||
}
|
||||
|
||||
if ($auth->isGranted('view_tag')) {
|
||||
$menu->addChild(
|
||||
new MenuItemModel('tags', 'menu.tags', 'tags', [], 'fas fa-tags')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
82
src/Form/DataTransformer/TagArrayToStringTransformer.php
Normal file
82
src/Form/DataTransformer/TagArrayToStringTransformer.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?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\DataTransformer;
|
||||
|
||||
use App\Entity\Tag;
|
||||
use App\Repository\TagRepository;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
|
||||
class TagArrayToStringTransformer implements DataTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @var TagRepository
|
||||
*/
|
||||
private $tagRepository;
|
||||
|
||||
/**
|
||||
* @param TagRepository $tagRepository
|
||||
*/
|
||||
public function __construct(TagRepository $tagRepository)
|
||||
{
|
||||
$this->tagRepository = $tagRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms an array of tags to a string.
|
||||
*
|
||||
* @param Tag[]|null $tags
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function transform($tags): string
|
||||
{
|
||||
if (empty($tags)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return implode(', ', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms a string to an array of tags.
|
||||
*
|
||||
* @param string $stringOfTags
|
||||
*
|
||||
* @return Tag[]
|
||||
* @throws TransformationFailedException if object (issue) is not found
|
||||
*/
|
||||
public function reverseTransform($stringOfTags): array
|
||||
{
|
||||
// check for empty tag list
|
||||
if (empty($stringOfTags)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names = array_filter(array_unique(array_map('trim', explode(',', $stringOfTags))));
|
||||
|
||||
// Get the current tags and find the new ones that should be created
|
||||
$tags = $this->tagRepository->findBy(['name' => $names]);
|
||||
|
||||
$newNames = array_diff($names, $tags);
|
||||
foreach ($newNames as $name) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
$tags[] = $tag;
|
||||
|
||||
// There's no need to persist these new tags because Doctrine does that automatically
|
||||
// thanks to the cascade={"persist"} option in the App\Entity\Timesheet::$tags property.
|
||||
}
|
||||
|
||||
// Return an array of tags to transform them back into a Doctrine Collection.
|
||||
// See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
return $tags;
|
||||
}
|
||||
}
|
||||
@@ -92,6 +92,5 @@ class SelectWithApiDataExtension extends AbstractTypeExtension
|
||||
{
|
||||
$resolver->setDefined(['api_data']);
|
||||
$resolver->setAllowedTypes('api_data', 'array');
|
||||
//$resolver->setDefault('api_data', []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Form\Type\DurationType;
|
||||
use App\Form\Type\FixedRateType;
|
||||
use App\Form\Type\HourlyRateType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Repository\ActivityRepository;
|
||||
@@ -250,8 +251,17 @@ class TimesheetEditForm extends AbstractType
|
||||
->add('description', TextareaType::class, [
|
||||
'label' => 'label.description',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
]);
|
||||
|
||||
$builder
|
||||
->add('tags', TagsInputType::class, [
|
||||
// documentation is for NelmioApiDocBundle
|
||||
'documentation' => [
|
||||
'type' => 'text',
|
||||
'description' => 'Tags for timesheet entry',
|
||||
],
|
||||
'required' => false,
|
||||
]);
|
||||
|
||||
if ($options['include_rate']) {
|
||||
$builder
|
||||
|
||||
@@ -14,6 +14,7 @@ use App\Form\Type\CustomerType;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Form\Type\PageSizeType;
|
||||
use App\Form\Type\ProjectType;
|
||||
use App\Form\Type\TagsInputType;
|
||||
use App\Form\Type\UserRoleType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\VisibilityType;
|
||||
@@ -208,4 +209,14 @@ abstract class AbstractToolbarForm extends AbstractType
|
||||
'empty_data' => 1
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface $builder
|
||||
*/
|
||||
protected function addTagInputField(FormBuilderInterface $builder)
|
||||
{
|
||||
$builder->add('tags', TagsInputType::class, [
|
||||
'required' => false
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class ExportToolbarForm extends AbstractToolbarForm
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addExportType($builder);
|
||||
$this->addTagInputField($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -30,6 +30,7 @@ class InvoiceToolbarForm extends AbstractToolbarForm
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addTagInputField($builder);
|
||||
}
|
||||
|
||||
protected function addTemplateChoice(FormBuilderInterface $builder)
|
||||
|
||||
37
src/Form/Toolbar/TagToolbarForm.php
Normal file
37
src/Form/Toolbar/TagToolbarForm.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?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\Toolbar;
|
||||
|
||||
use App\Repository\Query\TagQuery;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class TagToolbarForm extends AbstractToolbarForm
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$this->addPageSizeChoice($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => TagQuery::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ class TimesheetToolbarForm extends AbstractToolbarForm
|
||||
$this->addCustomerChoice($builder);
|
||||
$this->addProjectChoice($builder);
|
||||
$this->addActivityChoice($builder);
|
||||
$this->addTagInputField($builder);
|
||||
$this->addHiddenPagination($builder);
|
||||
}
|
||||
|
||||
|
||||
77
src/Form/Type/TagsInputType.php
Normal file
77
src/Form/Type/TagsInputType.php
Normal file
@@ -0,0 +1,77 @@
|
||||
<?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\Form\DataTransformer\TagArrayToStringTransformer;
|
||||
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
/**
|
||||
* Custom form field type to enter tags or use one of autocompleted field
|
||||
*/
|
||||
class TagsInputType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var TagArrayToStringTransformer
|
||||
*/
|
||||
private $transformer;
|
||||
|
||||
/**
|
||||
* @var UrlGeneratorInterface
|
||||
*/
|
||||
private $router;
|
||||
|
||||
/**
|
||||
* @param TagArrayToStringTransformer $transformer
|
||||
* @param UrlGeneratorInterface $router
|
||||
*/
|
||||
public function __construct(TagArrayToStringTransformer $transformer, UrlGeneratorInterface $router)
|
||||
{
|
||||
$this->transformer = $transformer;
|
||||
$this->router = $router;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->addModelTransformer(new CollectionToArrayTransformer(), true)
|
||||
->addModelTransformer($this->transformer, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => 'label.tag',
|
||||
'attr' => [
|
||||
'data-autocomplete-url' => $this->router->generate('get_tags'),
|
||||
'class' => 'js-autocomplete',
|
||||
'autocomplete' => 'off',
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
}
|
||||
52
src/Migrations/Version20190510205245.php
Normal file
52
src/Migrations/Version20190510205245.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* 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 DoctrineMigrations;
|
||||
|
||||
use App\Doctrine\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* New feature: tagging of timesheet records
|
||||
*
|
||||
* @version 1.0
|
||||
*/
|
||||
class Version20190510205245 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$timesheetTags = $schema->createTable('kimai2_timesheet_tags');
|
||||
$timesheetTags->addColumn('timesheet_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$timesheetTags->addColumn('tag_id', 'integer', ['length' => 11, 'notnull' => true]);
|
||||
$timesheetTags->addIndex(['timesheet_id'], 'IDX_E3284EFEABDD46BE');
|
||||
$timesheetTags->addIndex(['tag_id'], 'IDX_E3284EFEBAD26311');
|
||||
$timesheetTags->setPrimaryKey(['timesheet_id', 'tag_id']);
|
||||
|
||||
$tags = $schema->createTable('kimai2_tags');
|
||||
$tags->addColumn('id', 'integer', ['length' => 11, 'autoincrement' => true, 'notnull' => true]);
|
||||
$tags->addColumn('name', 'string', ['length' => 255, 'notnull' => true]);
|
||||
$tags->addUniqueIndex(['name'], 'UNIQ_27CAF54C5E237E06');
|
||||
$tags->setPrimaryKey(['id']);
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$tags = $schema->getTable('kimai2_tags');
|
||||
$tags->dropIndex('UNIQ_27CAF54C5E237E06');
|
||||
|
||||
$timesheetTags = $schema->getTable('kimai2_timesheet_tags');
|
||||
$timesheetTags->dropIndex('IDX_E3284EFEABDD46BE');
|
||||
$timesheetTags->dropIndex('IDX_E3284EFEBAD26311');
|
||||
|
||||
$schema->dropTable('kimai2_timesheet_tags');
|
||||
$schema->dropTable('kimai2_tags');
|
||||
}
|
||||
}
|
||||
14
src/Repository/Query/TagQuery.php
Normal file
14
src/Repository/Query/TagQuery.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?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\Repository\Query;
|
||||
|
||||
class TagQuery extends BaseQuery
|
||||
{
|
||||
}
|
||||
@@ -54,6 +54,10 @@ class TimesheetQuery extends ActivityQuery
|
||||
* @var DateRange
|
||||
*/
|
||||
protected $dateRange;
|
||||
/**
|
||||
* @var iterable
|
||||
*/
|
||||
protected $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
@@ -208,4 +212,31 @@ class TimesheetQuery extends ActivityQuery
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable
|
||||
*/
|
||||
public function getTags()
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param iterable $tags
|
||||
* @return $this
|
||||
*/
|
||||
public function setTags(iterable $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTags()
|
||||
{
|
||||
return !empty($this->tags) && count($this->tags) > 0;
|
||||
}
|
||||
}
|
||||
|
||||
83
src/Repository/TagRepository.php
Normal file
83
src/Repository/TagRepository.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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\Repository;
|
||||
|
||||
use App\Repository\Query\TagQuery;
|
||||
|
||||
class TagRepository extends AbstractRepository
|
||||
{
|
||||
/**
|
||||
* Find ids of the given tagNames separated by comma
|
||||
* @param string $tagNames
|
||||
* @return array
|
||||
*/
|
||||
public function findIdsByTagNameList($tagNames)
|
||||
{
|
||||
$qb = $this
|
||||
->createQueryBuilder('t')
|
||||
->select('t.id');
|
||||
$list = array_filter(array_unique(array_map('trim', explode(',', $tagNames))));
|
||||
$cnt = 0;
|
||||
foreach ($list as $listElem) {
|
||||
$qb
|
||||
->orWhere('t.name like :elem' . $cnt)
|
||||
->setParameter('elem' . $cnt, '%' . $listElem . '%');
|
||||
$cnt++;
|
||||
}
|
||||
|
||||
return array_column($qb->getQuery()->getScalarResult(), 'id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all tag names in an alphabetical order
|
||||
*
|
||||
* @param string $filter
|
||||
* @return array
|
||||
*/
|
||||
public function findAllTagNames($filter = null)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('t');
|
||||
|
||||
$qb
|
||||
->select('t.name')
|
||||
->addOrderBy('t.name', 'ASC');
|
||||
|
||||
if (null !== $filter) {
|
||||
$qb
|
||||
->andWhere('t.name LIKE :filter')
|
||||
->setParameter('filter', '%' . $filter . '%');
|
||||
}
|
||||
|
||||
return array_column($qb->getQuery()->getScalarResult(), 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of arrays with each inner array having the structure:
|
||||
* - id
|
||||
* - name
|
||||
* - amount
|
||||
*
|
||||
* @param TagQuery $query
|
||||
* @return array|\Doctrine\ORM\QueryBuilder|\Pagerfanta\Pagerfanta
|
||||
*/
|
||||
public function getTagCount(TagQuery $query)
|
||||
{
|
||||
$qb = $this->createQueryBuilder('tag');
|
||||
|
||||
$qb
|
||||
->select('tag.id, tag.name, count(timesheets.id) as amount')
|
||||
->leftJoin('tag.timesheets', 'timesheets')
|
||||
->addGroupBy('tag.id')
|
||||
->orderBy('tag.name')
|
||||
;
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
}
|
||||
@@ -332,12 +332,13 @@ class TimesheetRepository extends AbstractRepository
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('t', 'a', 'p', 'c', 'u')
|
||||
$qb->select('t', 'a', 'p', 'c', 'u', 'tags')
|
||||
->from(Timesheet::class, 't')
|
||||
->leftJoin('t.activity', 'a')
|
||||
->leftJoin('t.user', 'u')
|
||||
->leftJoin('t.project', 'p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->leftJoin('t.tags', 'tags')
|
||||
->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
|
||||
|
||||
if (null !== $query->getUser()) {
|
||||
@@ -382,6 +383,11 @@ class TimesheetRepository extends AbstractRepository
|
||||
}
|
||||
}
|
||||
|
||||
if ($query->hasTags()) {
|
||||
$qb->andWhere('tags.id IN (:tags)')
|
||||
->setParameter('tags', $query->getTags()->toArray());
|
||||
}
|
||||
|
||||
return $this->getBaseQueryResult($qb, $query);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user