Files
kimai2/src/Repository/TimesheetRepository.php
Kevin Papst 3ac72a5c89 enhanced plugin support (#634)
- refactored admin controller and templates
- plugin support for entity actions
- changed column mail to email in customer table
- refactored theme events
2019-03-11 14:46:30 +01:00

381 lines
12 KiB
PHP

<?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\Entity\Activity;
use App\Entity\Timesheet;
use App\Entity\User;
use App\Model\Statistic\Month;
use App\Model\Statistic\Year;
use App\Model\TimesheetStatistic;
use App\Repository\Query\TimesheetQuery;
use DateTime;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\QueryBuilder;
use Pagerfanta\Pagerfanta;
class TimesheetRepository extends AbstractRepository
{
public const STATS_QUERY_DURATION = 'duration';
public const STATS_QUERY_RATE = 'rate';
public const STATS_QUERY_USER = 'users';
public const STATS_QUERY_AMOUNT = 'amount';
public const STATS_QUERY_ACTIVE = 'active';
public const STATS_QUERY_MONTHLY = 'monthly';
/**
* @param Timesheet $timesheet
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function save(Timesheet $timesheet)
{
$entityManager = $this->getEntityManager();
$entityManager->persist($timesheet);
$entityManager->flush();
}
/**
* @param Timesheet $entry
* @return bool
* @throws RepositoryException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function stopRecording(Timesheet $entry)
{
if (null !== $entry->getEnd()) {
throw new RepositoryException('Timesheet entry already stopped');
}
// seems to be necessary so Doctrine will recognize a changed timestamp
$begin = clone $entry->getBegin();
$end = new \DateTime('now', $begin->getTimezone());
$entry->setBegin($begin);
$entry->setEnd($end);
$entityManager = $this->getEntityManager();
$entityManager->persist($entry);
$entityManager->flush();
return true;
}
/**
* @param string $type
* @param DateTime|null $begin
* @param DateTime|null $end
* @param User|null $user
* @return int|mixed
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getStatistic(string $type, ?DateTime $begin, ?DateTime $end, ?User $user)
{
switch ($type) {
case self::STATS_QUERY_ACTIVE:
return count($this->getActiveEntries($user));
case self::STATS_QUERY_MONTHLY:
return $this->getMonthlyStats($user, $begin, $end);
case self::STATS_QUERY_DURATION:
$what = 'SUM(t.duration)';
break;
case self::STATS_QUERY_RATE:
$what = 'SUM(t.rate)';
break;
case self::STATS_QUERY_USER:
$what = 'COUNT(DISTINCT(t.user))';
break;
case self::STATS_QUERY_AMOUNT:
$what = 'COUNT(t.id)';
break;
default:
throw new \InvalidArgumentException('Invalid query type: ' . $type);
}
return $this->queryTimeRange($what, $begin, $end, $user);
}
/**
* @param $select
* @param User $user
* @return int
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function queryThisMonth($select, User $user)
{
$begin = new DateTime('first day of this month 00:00:00');
$end = new DateTime('last day of this month 23:59:59');
return $this->queryTimeRange($select, $begin, $end, $user);
}
/**
* @param string $select
* @param DateTime|null $begin
* @param DateTime|null $end
* @param User|null $user
* @return int|mixed
* @throws \Doctrine\ORM\NonUniqueResultException
*/
protected function queryTimeRange(string $select, ?DateTime $begin, ?DateTime $end, ?User $user)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select($select)
->from(Timesheet::class, 't');
if (!empty($begin)) {
$qb
->andWhere($qb->expr()->gt('t.begin', ':from'))
->setParameter('from', $begin, Type::DATETIME);
}
if (!empty($end)) {
$qb
->andWhere($qb->expr()->lt('t.end', ':to'))
->setParameter('to', $end, Type::DATETIME);
}
if (null !== $user) {
$qb->andWhere('t.user = :user')
->setParameter('user', $user);
}
$result = $qb->getQuery()->getSingleScalarResult();
return empty($result) ? 0 : $result;
}
/**
* Fetch statistic data for one user.
*
* @param User $user
* @return TimesheetStatistic
* @throws \Doctrine\ORM\NonUniqueResultException
*/
public function getUserStatistics(User $user)
{
$durationTotal = $this->getStatistic(self::STATS_QUERY_DURATION, null, null, $user);
$recordsTotal = $this->getStatistic(self::STATS_QUERY_AMOUNT, null, null, $user);
$rateTotal = $this->getStatistic(self::STATS_QUERY_RATE, null, null, $user);
$amountMonth = $this->queryThisMonth('SUM(t.rate)', $user);
$durationMonth = $this->queryThisMonth('SUM(t.duration)', $user);
$firstEntry = $this->getEntityManager()
->createQuery('SELECT MIN(t.begin) FROM ' . Timesheet::class . ' t WHERE t.user = :user')
->setParameter('user', $user)
->getSingleScalarResult();
$stats = new TimesheetStatistic();
$stats->setAmountTotal($rateTotal);
$stats->setDurationTotal($durationTotal);
$stats->setAmountThisMonth($amountMonth);
$stats->setDurationThisMonth($durationMonth);
$stats->setFirstEntry(new DateTime($firstEntry));
$stats->setRecordsTotal($recordsTotal);
return $stats;
}
/**
* Returns an array of Year statistics.
*
* @param User|null $user
* @param DateTime|null $begin
* @param DateTime|null $end
* @return Year[]
*/
public function getMonthlyStats(User $user = null, ?DateTime $begin = null, ?DateTime $end = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('SUM(t.rate) as rate, SUM(t.duration) as duration, MONTH(t.begin) as month, YEAR(t.begin) as year')
->from(Timesheet::class, 't')
->where($qb->expr()->gt('t.begin', ':from'))
;
if (!empty($begin)) {
$qb->setParameter('from', $begin, Type::DATETIME);
} else {
$qb->setParameter('from', 0);
}
if (!empty($end)) {
$qb->andWhere($qb->expr()->lt('t.end', ':to'))
->setParameter('to', $end, Type::DATETIME);
} else {
$qb->andWhere($qb->expr()->isNotNull('t.end'));
}
if (null !== $user) {
$qb->andWhere('t.user = :user')
->setParameter('user', $user);
}
$qb
->orderBy('year', 'DESC')
->addOrderBy('month', 'ASC')
->groupBy('year')
->addGroupBy('month');
$years = [];
foreach ($qb->getQuery()->execute() as $statRow) {
$curYear = $statRow['year'];
if (!isset($years[$curYear])) {
$year = new Year($curYear);
for ($i = 1; $i < 13; $i++) {
$month = $i < 10 ? '0' . $i : (string) $i;
$year->setMonth(new Month($month));
}
$years[$curYear] = $year;
}
$month = new Month($statRow['month']);
$month->setTotalDuration($statRow['duration'])
->setTotalRate($statRow['rate']);
$years[$curYear]->setMonth($month);
}
return $years;
}
/**
* @param User $user
* @return Timesheet[]|null
*/
public function getActiveEntries(User $user = null)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('t', 'a', 'p', 'c')
->from(Timesheet::class, 't')
->join('t.activity', 'a')
->join('t.project', 'p')
->join('p.customer', 'c')
->where($qb->expr()->gt('t.begin', '0'))
->andWhere($qb->expr()->isNull('t.end'))
->orderBy('t.begin', 'DESC');
$params = [];
if (null !== $user) {
$qb->andWhere('t.user = :user');
$params['user'] = $user;
}
return $qb->getQuery()->execute($params);
}
/**
* @param User $user
* @param int $limit
* @return int
* @throws RepositoryException
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function stopActiveEntries(User $user, int $hardLimit)
{
$counter = 0;
$activeEntries = $this->getActiveEntries($user);
// reduce limit by one:
// this method is only called when a new entry is started
// -> all entries, including the new one must not exceed the $limit
$limit = $hardLimit - 1;
if (count($activeEntries) > $limit) {
$i = 1;
foreach ($activeEntries as $activeEntry) {
if ($i > $limit) {
if ($hardLimit > 1) {
throw new \Exception('timesheet.start.exceeded_limit');
}
$this->stopRecording($activeEntry);
$counter++;
}
$i++;
}
}
return $counter;
}
/**
* @param TimesheetQuery $query
* @return QueryBuilder|Pagerfanta|array
*/
public function findByQuery(TimesheetQuery $query)
{
$qb = $this->getEntityManager()->createQueryBuilder();
$qb->select('t', 'a', 'p', 'c', 'u')
->from(Timesheet::class, 't')
->leftJoin('t.activity', 'a')
->leftJoin('t.user', 'u')
->leftJoin('t.project', 'p')
->leftJoin('p.customer', 'c')
->orderBy('t.' . $query->getOrderBy(), $query->getOrder());
if (null !== $query->getUser()) {
$qb->andWhere('t.user = :user')
->setParameter('user', $query->getUser());
}
if (null !== $query->getBegin()) {
$qb->andWhere('t.begin >= :begin')
->setParameter('begin', $query->getBegin());
}
if (TimesheetQuery::STATE_RUNNING == $query->getState()) {
$qb->andWhere($qb->expr()->isNull('t.end'));
}
if (TimesheetQuery::STATE_STOPPED == $query->getState()) {
$qb->andWhere($qb->expr()->isNotNull('t.end'));
if (null !== $query->getEnd()) {
$qb->andWhere('t.end <= :end')
->setParameter('end', $query->getEnd());
}
} elseif (null !== $query->getEnd()) {
$qb->andWhere('t.begin <= :end')
->setParameter('end', $query->getEnd());
}
if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', true);
} elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) {
$qb->andWhere('t.exported = :exported')->setParameter('exported', false);
}
if (null !== $query->getActivity()) {
$qb->andWhere('t.activity = :activity')
->setParameter('activity', $query->getActivity());
}
if (null === $query->getActivity() || ($query->getActivity() instanceof Activity && null === $query->getActivity()->getProject())) {
if (null !== $query->getProject()) {
$qb->andWhere('t.project = :project')
->setParameter('project', $query->getProject());
} elseif (null !== $query->getCustomer()) {
$qb->andWhere('p.customer = :customer')
->setParameter('customer', $query->getCustomer());
}
}
return $this->getBaseQueryResult($qb, $query);
}
}