billable timesheets, inactive projects report, bookmark export search (#2503)
This commit is contained in:
53
src/Reporting/ProjectInactive/ProjectInactiveForm.php
Normal file
53
src/Reporting/ProjectInactive/ProjectInactiveForm.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Reporting\ProjectInactive;
|
||||
|
||||
use App\Form\Type\DateTimePickerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectInactiveForm extends AbstractType
|
||||
{
|
||||
/**
|
||||
* Simplify cross linking between pages by removing the block prefix.
|
||||
*
|
||||
* @return null|string
|
||||
*/
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('lastChange', DateTimePickerType::class, [
|
||||
'label' => 'label.last_record_before',
|
||||
'model_timezone' => $options['timezone'],
|
||||
'view_timezone' => $options['timezone'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectInactiveQuery::class,
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
46
src/Reporting/ProjectInactive/ProjectInactiveQuery.php
Normal file
46
src/Reporting/ProjectInactive/ProjectInactiveQuery.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?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\Reporting\ProjectInactive;
|
||||
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectInactiveQuery
|
||||
{
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $lastChange;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
|
||||
public function __construct(DateTime $lastChange, User $user)
|
||||
{
|
||||
$this->lastChange = clone $lastChange;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function getLastChange(): DateTime
|
||||
{
|
||||
return $this->lastChange;
|
||||
}
|
||||
|
||||
public function setLastChange(DateTime $lastChange): void
|
||||
{
|
||||
$this->lastChange = clone $lastChange;
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,22 @@
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Reporting\ProjectView;
|
||||
namespace App\Reporting;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Reporting\ProjectInactive\ProjectInactiveQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewModel;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Exception;
|
||||
|
||||
final class ProjectViewService
|
||||
final class ProjectStatisticService
|
||||
{
|
||||
private $repository;
|
||||
private $timesheetRepository;
|
||||
@@ -28,28 +34,58 @@ final class ProjectViewService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return ProjectViewModel[]
|
||||
* @throws Exception
|
||||
* @param ProjectInactiveQuery $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function getProjectView(ProjectViewQuery $query): array
|
||||
public function findInactiveProjects(ProjectInactiveQuery $query): array
|
||||
{
|
||||
$factory = new DateTimeFactory($query->getToday()->getTimezone());
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
$lastChange = clone $query->getLastChange();
|
||||
$now = new DateTime('now', $lastChange->getTimezone());
|
||||
|
||||
$begin = $factory->getStartOfWeek($today);
|
||||
$end = $factory->getEndOfWeek($today);
|
||||
$startMonth = (clone $begin)->modify('first day of this month');
|
||||
$endMonth = (clone $begin)->modify('last day of this month');
|
||||
$qb2 = $this->repository->createQueryBuilder('t1');
|
||||
$qb2
|
||||
->select('1')
|
||||
->from(Timesheet::class, 't')
|
||||
->andWhere('p = t.project')
|
||||
->andWhere($qb2->expr()->gte('t.begin', ':begin'))
|
||||
;
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p AS project')
|
||||
->addSelect('SUM(t.duration) AS totalDuration')
|
||||
->addSelect('SUM(t.rate) AS totalRate')
|
||||
->select('p, c')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->andWhere($qb->expr()->not($qb->expr()->exists($qb2)))
|
||||
->andWhere(
|
||||
$qb->expr()->orX(
|
||||
$qb->expr()->isNull('p.end'),
|
||||
$qb->expr()->gte('p.end', ':project_end')
|
||||
)
|
||||
)
|
||||
->setParameter('project_end', $now, Types::DATETIME_MUTABLE)
|
||||
->setParameter('begin', $lastChange, Types::DATETIME_MUTABLE)
|
||||
;
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return Project[]
|
||||
*/
|
||||
public function findProjectsForView(ProjectViewQuery $query): array
|
||||
{
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p')
|
||||
->leftJoin('p.customer', 'c')
|
||||
->leftJoin(Timesheet::class, 't', 'WITH', 'p.id = t.project')
|
||||
->andWhere($qb->expr()->eq('p.visible', true))
|
||||
->andWhere($qb->expr()->eq('c.visible', true))
|
||||
->andWhere(
|
||||
@@ -59,7 +95,6 @@ final class ProjectViewService
|
||||
)
|
||||
)
|
||||
->addGroupBy('p')
|
||||
->addGroupBy('t.project')
|
||||
->setParameter('project_end', $today, Types::DATETIME_MUTABLE)
|
||||
;
|
||||
|
||||
@@ -69,7 +104,10 @@ final class ProjectViewService
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoWork()) {
|
||||
$qb->andHaving($qb->expr()->gt('totalDuration', 0));
|
||||
$qb
|
||||
->leftJoin(Timesheet::class, 't', 'WITH', 'p.id = t.project')
|
||||
->andHaving($qb->expr()->gt('SUM(t.duration)', 0))
|
||||
;
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoBudget()) {
|
||||
@@ -83,23 +121,59 @@ final class ProjectViewService
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
$result = $qb->getQuery()->getResult();
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
* @param Project[] $projects
|
||||
* @param DateTime|null $today
|
||||
* @return ProjectViewModel[]
|
||||
*/
|
||||
public function getProjectView(User $user, array $projects, ?DateTime $today = null): array
|
||||
{
|
||||
$factory = new DateTimeFactory(new DateTimeZone($user->getTimezone()));
|
||||
if (null === $today) {
|
||||
$today = $factory->createDateTime();
|
||||
}
|
||||
|
||||
$today = clone $today;
|
||||
|
||||
$begin = $factory->getStartOfWeek($today);
|
||||
$end = $factory->getEndOfWeek($today);
|
||||
$startMonth = (clone $begin)->modify('first day of this month');
|
||||
$endMonth = (clone $begin)->modify('last day of this month');
|
||||
|
||||
$projectViews = [];
|
||||
foreach ($result as $res) {
|
||||
$entity = new ProjectViewModel($res['project']);
|
||||
$entity->setDurationTotal($res['totalDuration'] ?? 0);
|
||||
$entity->setRateTotal($res['totalRate'] ?? 0.00);
|
||||
|
||||
$projectViews[$entity->getProject()->getId()] = $entity;
|
||||
foreach ($projects as $project) {
|
||||
$projectViews[$project->getId()] = new ProjectViewModel($project);
|
||||
}
|
||||
|
||||
$projectIds = array_keys($projectViews);
|
||||
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, COUNT(t.id) as amount, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate, MAX(t.begin) as lastRecord')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->groupBy('t.project')
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationTotal($row['duration']);
|
||||
$projectViews[$row['id']]->setRateTotal($row['rate']);
|
||||
$projectViews[$row['id']]->setTimesheetCounter($row['amount']);
|
||||
if ($row['lastRecord'] !== null) {
|
||||
// might be the wrong timezone
|
||||
$projectViews[$row['id']]->setLastRecord($factory->createDateTime($row['lastRecord']));
|
||||
}
|
||||
}
|
||||
|
||||
// values for today
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) = :starting_date')
|
||||
->groupBy('t.project')
|
||||
@@ -109,13 +183,13 @@ final class ProjectViewService
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration']);
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration'] ?? 0);
|
||||
}
|
||||
|
||||
// values for the current week
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_date AND :end_date')
|
||||
->groupBy('t.project')
|
||||
@@ -132,7 +206,7 @@ final class ProjectViewService
|
||||
// values for the current month
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_month AND :end_month')
|
||||
->groupBy('t.project')
|
||||
@@ -146,10 +220,10 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||
}
|
||||
|
||||
// values for the all time (not exported)
|
||||
// values for all time (not exported)
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration, SUM(t.rate) AS rate')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->groupBy('t.project')
|
||||
@@ -163,10 +237,10 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setNotExportedRate($row['rate']);
|
||||
}
|
||||
|
||||
// values for the all time (not exported and billable)
|
||||
// values for all time (not exported and billable)
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration, SUM(t.rate) AS rate')
|
||||
->select('IDENTITY(t.project) AS id, COALESCE(SUM(t.duration), 0) AS duration, COALESCE(SUM(t.rate), 0) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->andWhere('t.billable = :billable')
|
||||
@@ -182,6 +256,23 @@ final class ProjectViewService
|
||||
$projectViews[$row['id']]->setNotBilledRate($row['rate']);
|
||||
}
|
||||
|
||||
// values for all time (none billable)
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration, SUM(t.rate) AS rate')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.billable = :billable')
|
||||
->groupBy('t.project')
|
||||
->setParameter('billable', true, Types::BOOLEAN)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setBillableDuration($row['duration']);
|
||||
$projectViews[$row['id']]->setBillableRate($row['rate']);
|
||||
}
|
||||
|
||||
return array_values($projectViews);
|
||||
}
|
||||
}
|
||||
@@ -10,49 +10,24 @@
|
||||
namespace App\Reporting\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $timesheetCounter = 0;
|
||||
private $durationDay = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationWeek = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationMonth = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationTotal = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $rateTotal = 0.00;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notExportedDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notExportedRate = 0.00;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notBilledDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notBilledRate = 0.00;
|
||||
private $billableDuration = 0;
|
||||
private $billableRate = 0.00;
|
||||
private $lastRecord;
|
||||
|
||||
public function __construct(Project $project)
|
||||
{
|
||||
@@ -64,6 +39,16 @@ final class ProjectViewModel
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function getTimesheetCounter(): int
|
||||
{
|
||||
return $this->timesheetCounter;
|
||||
}
|
||||
|
||||
public function setTimesheetCounter(int $timesheetCounter): void
|
||||
{
|
||||
$this->timesheetCounter = $timesheetCounter;
|
||||
}
|
||||
|
||||
public function getDurationDay(): int
|
||||
{
|
||||
return $this->durationDay;
|
||||
@@ -144,6 +129,26 @@ final class ProjectViewModel
|
||||
$this->notBilledRate = $notBilledRate;
|
||||
}
|
||||
|
||||
public function getBillableDuration(): int
|
||||
{
|
||||
return $this->billableDuration;
|
||||
}
|
||||
|
||||
public function setBillableDuration(int $billableDuration): void
|
||||
{
|
||||
$this->billableDuration = $billableDuration;
|
||||
}
|
||||
|
||||
public function getBillableRate(): float
|
||||
{
|
||||
return $this->billableRate;
|
||||
}
|
||||
|
||||
public function setBillableRate(float $billableRate): void
|
||||
{
|
||||
$this->billableRate = $billableRate;
|
||||
}
|
||||
|
||||
public function getRateTotal(): float
|
||||
{
|
||||
return $this->rateTotal;
|
||||
@@ -153,4 +158,14 @@ final class ProjectViewModel
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
}
|
||||
|
||||
public function getLastRecord(): ?DateTime
|
||||
{
|
||||
return $this->lastRecord;
|
||||
}
|
||||
|
||||
public function setLastRecord(DateTime $lastRecord): void
|
||||
{
|
||||
$this->lastRecord = $lastRecord;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ final class ReportingService
|
||||
}
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view', 'project'));
|
||||
$event->addReport(new Report('inactive_projects', 'report_project_inactive', 'report_inactive_project', 'project'));
|
||||
}
|
||||
|
||||
$this->dispatcher->dispatch($event);
|
||||
|
||||
Reference in New Issue
Block a user