added new report: project view (#1738)
This commit is contained in:
committed by
GitHub
parent
0b7d551048
commit
c34e9f576d
52
src/Controller/Reporting/ProjectViewController.php
Normal file
52
src/Controller/Reporting/ProjectViewController.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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\Reporting;
|
||||
|
||||
use App\Controller\AbstractController;
|
||||
use App\Reporting\ProjectView\ProjectViewForm;
|
||||
use App\Reporting\ProjectView\ProjectViewQuery;
|
||||
use App\Reporting\ProjectView\ProjectViewService;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
final class ProjectViewController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route(path="/reporting/project_view", name="report_project_view", methods={"GET","POST"})
|
||||
* @Security("is_granted('view_reporting') and is_granted('budget_project')")
|
||||
*/
|
||||
public function __invoke(Request $request, ProjectViewService $service)
|
||||
{
|
||||
$query = new ProjectViewQuery($this->getDateTimeFactory()->createDateTime(), $this->getUser());
|
||||
|
||||
$form = $this->createForm(ProjectViewForm::class, $query, [
|
||||
'action' => $this->generateUrl('report_project_view')
|
||||
]);
|
||||
|
||||
$form->submit($request->query->all(), false);
|
||||
|
||||
$entries = $service->getProjectView($query);
|
||||
|
||||
$byCustomer = [];
|
||||
foreach ($entries as $entry) {
|
||||
$customer = $entry->getProject()->getCustomer();
|
||||
if (!isset($byCustomer[$customer->getId()])) {
|
||||
$byCustomer[$customer->getId()] = ['customer' => $customer, 'projects' => []];
|
||||
}
|
||||
$byCustomer[$customer->getId()]['projects'][] = $entry;
|
||||
}
|
||||
|
||||
return $this->render('reporting/project_view.html.twig', [
|
||||
'entries' => $byCustomer,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
61
src/Reporting/ProjectView/ProjectViewForm.php
Normal file
61
src/Reporting/ProjectView/ProjectViewForm.php
Normal file
@@ -0,0 +1,61 @@
|
||||
<?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\ProjectView;
|
||||
|
||||
use App\Form\Type\CustomerType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ProjectViewForm 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('customer', CustomerType::class, [
|
||||
'required' => false,
|
||||
'label' => false,
|
||||
'width' => false,
|
||||
]);
|
||||
$builder->add('includeNoBudget', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoBudget',
|
||||
]);
|
||||
$builder->add('includeNoWork', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'label.includeNoWork',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => ProjectViewQuery::class,
|
||||
'csrf_protection' => false,
|
||||
'method' => 'GET',
|
||||
]);
|
||||
}
|
||||
}
|
||||
128
src/Reporting/ProjectView/ProjectViewModel.php
Normal file
128
src/Reporting/ProjectView/ProjectViewModel.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?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\ProjectView;
|
||||
|
||||
use App\Entity\Project;
|
||||
|
||||
final class ProjectViewModel
|
||||
{
|
||||
/**
|
||||
* @var Project
|
||||
*/
|
||||
private $project;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationDay = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationWeek = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationMonth = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $durationTotal = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $rateTotal = 0;
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $notExportedDuration = 0;
|
||||
/**
|
||||
* @var float
|
||||
*/
|
||||
private $notExportedRate = 0.00;
|
||||
|
||||
public function getProject(): ?Project
|
||||
{
|
||||
return $this->project;
|
||||
}
|
||||
|
||||
public function setProject(Project $project): void
|
||||
{
|
||||
$this->project = $project;
|
||||
}
|
||||
|
||||
public function getDurationDay(): int
|
||||
{
|
||||
return $this->durationDay;
|
||||
}
|
||||
|
||||
public function setDurationDay(int $durationDay): void
|
||||
{
|
||||
$this->durationDay = $durationDay;
|
||||
}
|
||||
|
||||
public function getDurationWeek(): int
|
||||
{
|
||||
return $this->durationWeek;
|
||||
}
|
||||
|
||||
public function setDurationWeek(int $durationWeek): void
|
||||
{
|
||||
$this->durationWeek = $durationWeek;
|
||||
}
|
||||
|
||||
public function getDurationMonth(): int
|
||||
{
|
||||
return $this->durationMonth;
|
||||
}
|
||||
|
||||
public function setDurationMonth(int $durationMonth): void
|
||||
{
|
||||
$this->durationMonth = $durationMonth;
|
||||
}
|
||||
|
||||
public function getDurationTotal(): int
|
||||
{
|
||||
return $this->durationTotal;
|
||||
}
|
||||
|
||||
public function setDurationTotal(int $durationTotal): void
|
||||
{
|
||||
$this->durationTotal = $durationTotal;
|
||||
}
|
||||
|
||||
public function getNotExportedDuration(): int
|
||||
{
|
||||
return $this->notExportedDuration;
|
||||
}
|
||||
|
||||
public function setNotExportedDuration(int $notExportedDuration): void
|
||||
{
|
||||
$this->notExportedDuration = $notExportedDuration;
|
||||
}
|
||||
|
||||
public function getNotExportedRate(): float
|
||||
{
|
||||
return $this->notExportedRate;
|
||||
}
|
||||
|
||||
public function setNotExportedRate(float $notExportedRate): void
|
||||
{
|
||||
$this->notExportedRate = $notExportedRate;
|
||||
}
|
||||
|
||||
public function getRateTotal(): int
|
||||
{
|
||||
return $this->rateTotal;
|
||||
}
|
||||
|
||||
public function setRateTotal(int $rateTotal): void
|
||||
{
|
||||
$this->rateTotal = $rateTotal;
|
||||
}
|
||||
}
|
||||
84
src/Reporting/ProjectView/ProjectViewQuery.php
Normal file
84
src/Reporting/ProjectView/ProjectViewQuery.php
Normal file
@@ -0,0 +1,84 @@
|
||||
<?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\ProjectView;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\User;
|
||||
use DateTime;
|
||||
|
||||
final class ProjectViewQuery
|
||||
{
|
||||
/**
|
||||
* @var Customer|null
|
||||
*/
|
||||
private $customer;
|
||||
/**
|
||||
* @var DateTime
|
||||
*/
|
||||
private $today;
|
||||
/**
|
||||
* @var User|null
|
||||
*/
|
||||
private $user;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoBudget = false;
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $includeNoWork = false;
|
||||
|
||||
public function __construct(DateTime $today, User $user)
|
||||
{
|
||||
$this->today = $today;
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
public function isIncludeNoBudget(): bool
|
||||
{
|
||||
return $this->includeNoBudget;
|
||||
}
|
||||
|
||||
public function setIncludeNoBudget(bool $includeNoBudget): void
|
||||
{
|
||||
$this->includeNoBudget = $includeNoBudget;
|
||||
}
|
||||
|
||||
public function isIncludeNoWork(): bool
|
||||
{
|
||||
return $this->includeNoWork;
|
||||
}
|
||||
|
||||
public function setIncludeNoWork(bool $includeNoWork): void
|
||||
{
|
||||
$this->includeNoWork = $includeNoWork;
|
||||
}
|
||||
|
||||
public function getCustomer(): ?Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
|
||||
public function setCustomer(Customer $customer): void
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function getToday(): DateTime
|
||||
{
|
||||
return $this->today;
|
||||
}
|
||||
}
|
||||
157
src/Reporting/ProjectView/ProjectViewService.php
Normal file
157
src/Reporting/ProjectView/ProjectViewService.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?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\ProjectView;
|
||||
|
||||
use App\Entity\Timesheet;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Timesheet\DateTimeFactory;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Exception;
|
||||
|
||||
final class ProjectViewService
|
||||
{
|
||||
private $repository;
|
||||
private $timesheetRepository;
|
||||
|
||||
public function __construct(ProjectRepository $projectRepository, TimesheetRepository $timesheetRepository)
|
||||
{
|
||||
$this->repository = $projectRepository;
|
||||
$this->timesheetRepository = $timesheetRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ProjectViewQuery $query
|
||||
* @return ProjectViewModel[]
|
||||
* @throws Exception
|
||||
*/
|
||||
public function getProjectView(ProjectViewQuery $query): array
|
||||
{
|
||||
$factory = new DateTimeFactory($query->getToday()->getTimezone());
|
||||
$user = $query->getUser();
|
||||
$today = clone $query->getToday();
|
||||
|
||||
$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');
|
||||
|
||||
$qb = $this->repository->createQueryBuilder('p');
|
||||
$qb
|
||||
->select('p AS project')
|
||||
->addSelect('SUM(t.duration) AS totalDuration')
|
||||
->addSelect('SUM(t.rate) AS totalRate')
|
||||
->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))
|
||||
->addGroupBy('p')
|
||||
->addGroupBy('t.project')
|
||||
;
|
||||
|
||||
if ($query->getCustomer() !== null) {
|
||||
$qb->andWhere($qb->expr()->eq('c', ':customer'));
|
||||
$qb->setParameter('customer', $query->getCustomer()->getId());
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoWork()) {
|
||||
$qb->andHaving($qb->expr()->gt('totalDuration', 0));
|
||||
}
|
||||
|
||||
if (!$query->isIncludeNoBudget()) {
|
||||
$qb->andWhere($qb->expr()->gt('p.timeBudget', 0));
|
||||
}
|
||||
|
||||
$this->repository->addPermissionCriteria($qb, $user);
|
||||
|
||||
$result = $qb->getQuery()->getResult();
|
||||
|
||||
$projectViews = [];
|
||||
foreach ($result as $res) {
|
||||
$entity = new ProjectViewModel();
|
||||
$entity->setProject($res['project']);
|
||||
$entity->setDurationTotal($res['totalDuration'] ?? 0);
|
||||
$entity->setRateTotal($res['totalRate'] ?? 0);
|
||||
|
||||
$projectViews[$entity->getProject()->getId()] = $entity;
|
||||
}
|
||||
|
||||
$projectIds = array_keys($projectViews);
|
||||
|
||||
// values for today
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) = :starting_date')
|
||||
->groupBy('t.project')
|
||||
->setParameter('starting_date', $today->format('Y-m-d'))
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationDay($row['duration']);
|
||||
}
|
||||
|
||||
// values for the current week
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_date AND :end_date')
|
||||
->groupBy('t.project')
|
||||
->setParameter('start_date', $begin->format('Y-m-d'))
|
||||
->setParameter('end_date', $end->format('Y-m-d'))
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationWeek($row['duration']);
|
||||
}
|
||||
|
||||
// values for the current month
|
||||
$qb = $this->timesheetRepository->createQueryBuilder('t');
|
||||
$qb
|
||||
->select('IDENTITY(t.project) AS id, SUM(t.duration) AS duration')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('DATE(t.begin) BETWEEN :start_month AND :end_month')
|
||||
->groupBy('t.project')
|
||||
->setParameter('start_month', $startMonth)
|
||||
->setParameter('end_month', $endMonth)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setDurationMonth($row['duration']);
|
||||
}
|
||||
|
||||
// values for the 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')
|
||||
->andWhere($qb->expr()->in('t.project', ':project'))
|
||||
->andWhere('t.exported = :exported')
|
||||
->groupBy('t.project')
|
||||
->setParameter('exported', false, Types::BOOLEAN)
|
||||
->setParameter('project', array_values($projectIds))
|
||||
;
|
||||
|
||||
$result = $qb->getQuery()->getScalarResult();
|
||||
foreach ($result as $row) {
|
||||
$projectViews[$row['id']]->setNotExportedDuration($row['duration']);
|
||||
$projectViews[$row['id']]->setNotExportedRate($row['rate']);
|
||||
}
|
||||
|
||||
return array_values($projectViews);
|
||||
}
|
||||
}
|
||||
@@ -11,17 +11,8 @@ namespace App\Reporting;
|
||||
|
||||
final class Report implements ReportInterface
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $id;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $label;
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $route;
|
||||
|
||||
public function __construct(string $id, string $route, string $label)
|
||||
|
||||
@@ -42,6 +42,9 @@ final class ReportingService
|
||||
if ($this->security->isGranted('view_reporting')) {
|
||||
$event->addReport(new Report('week_by_user', 'report_user_week', 'report_user_week'));
|
||||
$event->addReport(new Report('month_by_user', 'report_user_month', 'report_user_month'));
|
||||
if ($this->security->isGranted('budget_project')) {
|
||||
$event->addReport(new Report('project_view', 'report_project_view', 'report_project_view'));
|
||||
}
|
||||
if ($this->security->isGranted('view_other_timesheet')) {
|
||||
$event->addReport(new Report('monthly_users_list', 'report_monthly_users', 'report_monthly_users'));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ use App\Repository\Paginator\LoaderPaginator;
|
||||
use App\Repository\Paginator\PaginatorInterface;
|
||||
use App\Repository\Query\ProjectFormTypeQuery;
|
||||
use App\Repository\Query\ProjectQuery;
|
||||
use DateTime;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\ORMException;
|
||||
use Doctrine\ORM\Query;
|
||||
@@ -77,7 +78,7 @@ class ProjectRepository extends EntityRepository
|
||||
return $this->count([]);
|
||||
}
|
||||
|
||||
public function getProjectStatistics(Project $project, ?\DateTime $begin = null, ?\DateTime $end = null): ProjectStatistic
|
||||
public function getProjectStatistics(Project $project, ?DateTime $begin = null, ?DateTime $end = null): ProjectStatistic
|
||||
{
|
||||
$stats = new ProjectStatistic($project);
|
||||
|
||||
@@ -127,7 +128,7 @@ class ProjectRepository extends EntityRepository
|
||||
return $stats;
|
||||
}
|
||||
|
||||
private function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
public function addPermissionCriteria(QueryBuilder $qb, ?User $user = null, array $teams = [])
|
||||
{
|
||||
// make sure that all queries without a user see all projects
|
||||
if (null === $user && empty($teams)) {
|
||||
@@ -203,7 +204,7 @@ class ProjectRepository extends EntityRepository
|
||||
$qb->andWhere($qb->expr()->eq('c.visible', ':customer_visible'));
|
||||
|
||||
if (!$query->isIgnoreDate()) {
|
||||
$now = new \DateTime();
|
||||
$now = new DateTime();
|
||||
$qb->andWhere(
|
||||
$qb->expr()->andX(
|
||||
$qb->expr()->orX(
|
||||
|
||||
Reference in New Issue
Block a user