added project start and end date (#1303)
* added sortable js library * activity in invoice is optional * added javascript widget for paginated boxes * fix activity dropdown for globals only * added timesheet service to reduce code duplication * use repository to query for teams in dropdowns * added project validator * validate project start and end against timesheet * include begin and end in dynamic form requests for projects * added timezone and language option to import flag, improve timesheet import speed * deactivate cross-timezone filter * add virtual fields to field order list * composer update * added param to ignore dates * position loader icon fixed - fixes #1330 * permission problem when creating a new project - fixes #1340 * remove dev dependencies webserver and thanks bundle * stop information leak (begin and end date) in duration mode - fixes #1307 * unify timesheet edit dialog for user and admins * fix security issue, own rates exposed to unauthorized users in multi-update dialog
This commit is contained in:
@@ -182,6 +182,36 @@ class ProjectRepository extends EntityRepository
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('p.visible', ':visible'));
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
|
||||
if (!$query->isIgnoreDate()) {
|
||||
$now = new \DateTime();
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':start'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':start'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
)
|
||||
)
|
||||
)->setParameter('start', $now);
|
||||
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':end'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':end'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
)
|
||||
)
|
||||
)->setParameter('end', $now);
|
||||
}
|
||||
|
||||
$qb->setParameter('visible', true, \PDO::PARAM_BOOL);
|
||||
$qb->setParameter('customer_visible', true, \PDO::PARAM_BOOL);
|
||||
|
||||
@@ -246,6 +276,47 @@ class ProjectRepository extends EntityRepository
|
||||
->setParameter('customer', $query->getCustomer());
|
||||
}
|
||||
|
||||
// this is far from being perfect, possible enhancements:
|
||||
// there could also be a range selection to be able to select all projects that were active between from and to
|
||||
// begin = null and end = null
|
||||
// begin = null and end <= to
|
||||
// begin < to and end = null
|
||||
// begin > from and end < to
|
||||
// ... and more ...
|
||||
|
||||
$begin = $query->getProjectStart();
|
||||
$end = $query->getProjectEnd();
|
||||
|
||||
if (null !== $begin) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':start'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':start'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
)
|
||||
)
|
||||
)->setParameter('start', $query->getProjectStart());
|
||||
}
|
||||
|
||||
if (null !== $end) {
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->gte('p.end', ':end'),
|
||||
$qb->expr()->isNull('p.end')
|
||||
),
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->lte('p.start', ':end'),
|
||||
$qb->expr()->isNull('p.start')
|
||||
)
|
||||
)
|
||||
)->setParameter('end', $query->getProjectEnd());
|
||||
}
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser());
|
||||
|
||||
if ($query->hasSearchTerm()) {
|
||||
|
||||
@@ -92,6 +92,12 @@ final class ActivityFormTypeQuery
|
||||
|
||||
public function isGlobalsOnly(): bool
|
||||
{
|
||||
return null === $this->activity && null === $this->project;
|
||||
return
|
||||
(
|
||||
null === $this->activity ||
|
||||
($this->activity instanceof Activity && null === $this->activity->getProject())
|
||||
)
|
||||
&&
|
||||
null === $this->project;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@ class BaseQuery
|
||||
public const DEFAULT_PAGE = 1;
|
||||
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
* @deprecated since 1.4, will be removed with 2.0
|
||||
*/
|
||||
public const RESULT_TYPE_OBJECTS = 'Objects';
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
* @deprecated since 1.4, will be removed with 2.0
|
||||
*/
|
||||
public const RESULT_TYPE_PAGER = 'PagerFanta';
|
||||
/**
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
* @deprecated since 1.4, will be removed with 2.0
|
||||
*/
|
||||
public const RESULT_TYPE_QUERYBUILDER = 'QueryBuilder';
|
||||
|
||||
@@ -63,7 +63,7 @@ class BaseQuery
|
||||
private $order = self::ORDER_ASC;
|
||||
/**
|
||||
* @var string
|
||||
* @deprecated since 1.4, will be removed with 1.6
|
||||
* @deprecated since 1.4, will be removed with 2.0
|
||||
*/
|
||||
private $resultType = self::RESULT_TYPE_PAGER;
|
||||
/**
|
||||
@@ -79,6 +79,23 @@ class BaseQuery
|
||||
*/
|
||||
private $searchTerm;
|
||||
|
||||
/**
|
||||
* @param Team[] $teams
|
||||
* @return $this
|
||||
*/
|
||||
public function setTeams(?array $teams): self
|
||||
{
|
||||
$this->teams = [];
|
||||
|
||||
if (null !== $teams) {
|
||||
foreach ($teams as $team) {
|
||||
$this->addTeam($team);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function addTeam(Team $team): self
|
||||
{
|
||||
$this->teams[$team->getId()] = $team;
|
||||
@@ -189,7 +206,7 @@ class BaseQuery
|
||||
*/
|
||||
public function getResultType()
|
||||
{
|
||||
@trigger_error('BaseQuery::getResultType() is deprecated and will be removed with 1.6', E_USER_DEPRECATED);
|
||||
@trigger_error('BaseQuery::getResultType() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
|
||||
return $this->resultType;
|
||||
}
|
||||
@@ -254,4 +271,27 @@ class BaseQuery
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function copyTo(BaseQuery $query): BaseQuery
|
||||
{
|
||||
$query->setDefaults($this->defaults);
|
||||
if (null !== $this->getCurrentUser()) {
|
||||
$query->setCurrentUser($this->getCurrentUser());
|
||||
}
|
||||
$query->setOrder($this->getOrder());
|
||||
$query->setOrderBy($this->getOrderBy());
|
||||
$query->setSearchTerm($this->getSearchTerm());
|
||||
$query->setPage($this->getPage());
|
||||
$query->setPageSize($this->getPageSize());
|
||||
|
||||
foreach ($this->getTeams() as $team) {
|
||||
$query->addTeam($team);
|
||||
}
|
||||
|
||||
if ($this instanceof VisibilityInterface && $query instanceof VisibilityInterface) {
|
||||
$query->setVisibility($this->getVisibility());
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ namespace App\Repository\Query;
|
||||
/**
|
||||
* Can be used for advanced queries with the: CustomerRepository
|
||||
*/
|
||||
class CustomerQuery extends VisibilityQuery
|
||||
class CustomerQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
|
||||
public const CUSTOMER_ORDER_ALLOWED = ['id', 'name', 'comment', 'country', 'number'];
|
||||
|
||||
public function __construct()
|
||||
|
||||
@@ -36,6 +36,10 @@ final class ProjectFormTypeQuery
|
||||
* @var array<Team>
|
||||
*/
|
||||
private $teams = [];
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $ignoreDate = false;
|
||||
|
||||
/**
|
||||
* @param Project|int|null $project
|
||||
@@ -126,4 +130,16 @@ final class ProjectFormTypeQuery
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isIgnoreDate(): bool
|
||||
{
|
||||
return $this->ignoreDate;
|
||||
}
|
||||
|
||||
public function setIgnoreDate(bool $ignoreDate): ProjectFormTypeQuery
|
||||
{
|
||||
$this->ignoreDate = $ignoreDate;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,18 +14,27 @@ use App\Entity\Customer;
|
||||
/**
|
||||
* Can be used for advanced queries with the: ProjectRepository
|
||||
*/
|
||||
class ProjectQuery extends CustomerQuery
|
||||
class ProjectQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
public const PROJECT_ORDER_ALLOWED = ['id', 'name', 'comment', 'customer', 'orderNumber'];
|
||||
use VisibilityTrait;
|
||||
|
||||
public const PROJECT_ORDER_ALLOWED = ['id', 'name', 'comment', 'customer', 'orderNumber', 'projectStart', 'projectEnd'];
|
||||
|
||||
/**
|
||||
* @var Customer|int|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $projectStart;
|
||||
/**
|
||||
* @var \DateTime
|
||||
*/
|
||||
private $projectEnd;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
$this->setDefaults([
|
||||
'orderBy' => 'name',
|
||||
]);
|
||||
@@ -49,4 +58,28 @@ class ProjectQuery extends CustomerQuery
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProjectStart(): ?\DateTime
|
||||
{
|
||||
return $this->projectStart;
|
||||
}
|
||||
|
||||
public function setProjectStart(?\DateTime $projectStart): ProjectQuery
|
||||
{
|
||||
$this->projectStart = $projectStart;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getProjectEnd(): ?\DateTime
|
||||
{
|
||||
return $this->projectEnd;
|
||||
}
|
||||
|
||||
public function setProjectEnd(?\DateTime $projectEnd): ProjectQuery
|
||||
{
|
||||
$this->projectEnd = $projectEnd;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,10 @@ namespace App\Repository\Query;
|
||||
/**
|
||||
* Can be used for advanced queries with the: UserRepository
|
||||
*/
|
||||
class UserQuery extends VisibilityQuery
|
||||
class UserQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
use VisibilityTrait;
|
||||
|
||||
public const USER_ORDER_ALLOWED = ['id', 'alias', 'username', 'title', 'email'];
|
||||
|
||||
/**
|
||||
|
||||
31
src/Repository/Query/VisibilityInterface.php
Normal file
31
src/Repository/Query/VisibilityInterface.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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;
|
||||
|
||||
interface VisibilityInterface
|
||||
{
|
||||
public const SHOW_VISIBLE = 1;
|
||||
public const SHOW_HIDDEN = 2;
|
||||
public const SHOW_BOTH = 3;
|
||||
|
||||
public const ALLOWED_VISIBILITY_STATES = [
|
||||
self::SHOW_BOTH,
|
||||
self::SHOW_VISIBLE,
|
||||
self::SHOW_HIDDEN,
|
||||
];
|
||||
|
||||
public function getVisibility(): int;
|
||||
|
||||
/**
|
||||
* @param int $visibility
|
||||
* @return mixed
|
||||
*/
|
||||
public function setVisibility($visibility);
|
||||
}
|
||||
@@ -11,43 +11,10 @@ namespace App\Repository\Query;
|
||||
|
||||
/**
|
||||
* Query class for Repositories with a visibility field.
|
||||
*
|
||||
* @deprecated since 1.7, will be removed with 2.0
|
||||
*/
|
||||
class VisibilityQuery extends BaseQuery
|
||||
class VisibilityQuery extends BaseQuery implements VisibilityInterface
|
||||
{
|
||||
public const SHOW_VISIBLE = 1;
|
||||
public const SHOW_HIDDEN = 2;
|
||||
public const SHOW_BOTH = 3;
|
||||
|
||||
public const ALLOWED_VISIBILITY_STATES = [
|
||||
self::SHOW_BOTH,
|
||||
self::SHOW_VISIBLE,
|
||||
self::SHOW_HIDDEN,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $visibility = self::SHOW_VISIBLE;
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getVisibility()
|
||||
{
|
||||
return $this->visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $visibility
|
||||
* @return $this
|
||||
*/
|
||||
public function setVisibility($visibility)
|
||||
{
|
||||
$visibility = (int) $visibility;
|
||||
if (in_array($visibility, self::ALLOWED_VISIBILITY_STATES, true)) {
|
||||
$this->visibility = $visibility;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
use VisibilityTrait;
|
||||
}
|
||||
|
||||
33
src/Repository/Query/VisibilityTrait.php
Normal file
33
src/Repository/Query/VisibilityTrait.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\Repository\Query;
|
||||
|
||||
trait VisibilityTrait
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $visibility = VisibilityInterface::SHOW_VISIBLE;
|
||||
|
||||
public function getVisibility(): int
|
||||
{
|
||||
return $this->visibility;
|
||||
}
|
||||
|
||||
public function setVisibility($visibility)
|
||||
{
|
||||
$visibility = (int) $visibility;
|
||||
if (in_array($visibility, VisibilityInterface::ALLOWED_VISIBILITY_STATES, true)) {
|
||||
$this->visibility = $visibility;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,25 @@ class TeamRepository extends EntityRepository
|
||||
$entityManager->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a query builder that is used for TeamType and your own 'query_builder' option.
|
||||
*
|
||||
* @param TeamQuery $query
|
||||
* @return QueryBuilder
|
||||
*/
|
||||
public function getQueryBuilderForFormType(TeamQuery $query): QueryBuilder
|
||||
{
|
||||
$qb = $this->getEntityManager()->createQueryBuilder();
|
||||
|
||||
$qb->select('t')
|
||||
->from(Team::class, 't')
|
||||
->orderBy('t.name', 'ASC');
|
||||
|
||||
$this->addPermissionCriteria($qb, $query->getCurrentUser(), $query->getTeams());
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
public function getPagerfantaForQuery(TeamQuery $query): Pagerfanta
|
||||
{
|
||||
$paginator = new Pagerfanta($this->getPaginatorForQuery($query));
|
||||
@@ -134,6 +153,11 @@ class TeamRepository extends EntityRepository
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param QueryBuilder $qb
|
||||
* @param User|null $user
|
||||
* @param Team[] $teams
|
||||
*/
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all user
|
||||
@@ -146,10 +170,21 @@ class TeamRepository extends EntityRepository
|
||||
return;
|
||||
}
|
||||
|
||||
$or = $qb->expr()->orX();
|
||||
|
||||
if (null !== $user) {
|
||||
$qb
|
||||
->andWhere('t.teamlead = :id')
|
||||
->setParameter('id', $user);
|
||||
$or->add($qb->expr()->eq('t.teamlead', ':id'));
|
||||
$qb->setParameter('id', $user);
|
||||
}
|
||||
|
||||
if (!empty($teams)) {
|
||||
$ids = [];
|
||||
foreach ($teams as $team) {
|
||||
$ids[] = $team->getId();
|
||||
}
|
||||
$or->add($qb->expr()->in('t.id', $ids));
|
||||
}
|
||||
|
||||
$qb->andWhere($or);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -827,6 +827,11 @@ class TimesheetRepository extends EntityRepository
|
||||
|
||||
private function getDatetimeFieldSql(string $field): string
|
||||
{
|
||||
return sprintf('CONVERT_TZ(%s, \'UTC\', t.timezone)', $field);
|
||||
// this would change the selected data for queries that join across multiple timezones
|
||||
// but due to tax laws, this is disabled - exports/invoices should *always* include the data from
|
||||
// the own timezone, not from the original users timezone
|
||||
// return sprintf('CONVERT_TZ(%s, \'UTC\', t.timezone)', $field);
|
||||
|
||||
return $field;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface
|
||||
*/
|
||||
public function findByQuery(UserQuery $query)
|
||||
{
|
||||
@trigger_error('UserRepository::findByQuery() is deprecated and will be removed with 1.6', E_USER_DEPRECATED);
|
||||
@trigger_error('UserRepository::findByQuery() is deprecated and will be removed with 2.0', E_USER_DEPRECATED);
|
||||
$qb = $this->getQueryBuilderForQuery($query);
|
||||
|
||||
if (BaseQuery::RESULT_TYPE_PAGER === $query->getResultType()) {
|
||||
|
||||
Reference in New Issue
Block a user