added project filter in user-list reports (#4615)

This commit is contained in:
Kevin Papst
2024-02-07 18:00:29 +01:00
committed by GitHub
parent e98732f44a
commit 7abe787778
13 changed files with 139 additions and 60 deletions

View File

@@ -1032,11 +1032,6 @@ parameters:
count: 1 count: 1
path: src/Controller/Reporting/CustomerMonthlyProjectsController.php path: src/Controller/Reporting/CustomerMonthlyProjectsController.php
-
message: "#^Parameter \\#3 \\$users of method App\\\\Reporting\\\\CustomerMonthlyProjects\\\\CustomerMonthlyProjectsRepository\\:\\:getGroupedByCustomerProjectActivityUser\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1
path: src/Controller/Reporting/CustomerMonthlyProjectsController.php
- -
message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#"
count: 3 count: 3
@@ -1052,31 +1047,16 @@ parameters:
count: 1 count: 1
path: src/Controller/Reporting/ReportUsersMonthController.php path: src/Controller/Reporting/ReportUsersMonthController.php
-
message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getDailyStatistics\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1
path: src/Controller/Reporting/ReportUsersMonthController.php
- -
message: "#^Method App\\\\Controller\\\\Reporting\\\\ReportUsersWeekController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Controller\\\\Reporting\\\\ReportUsersWeekController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
path: src/Controller/Reporting/ReportUsersWeekController.php path: src/Controller/Reporting/ReportUsersWeekController.php
-
message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getDailyStatistics\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1
path: src/Controller/Reporting/ReportUsersWeekController.php
- -
message: "#^Method App\\\\Controller\\\\Reporting\\\\ReportUsersYearController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Controller\\\\Reporting\\\\ReportUsersYearController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
path: src/Controller/Reporting/ReportUsersYearController.php path: src/Controller/Reporting/ReportUsersYearController.php
-
message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getMonthlyStats\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1
path: src/Controller/Reporting/ReportUsersYearController.php
- -
message: "#^Method App\\\\Controller\\\\Reporting\\\\UserMonthController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#" message: "#^Method App\\\\Controller\\\\Reporting\\\\UserMonthController\\:\\:getData\\(\\) return type has no value type specified in iterable type array\\.$#"
count: 1 count: 1
@@ -1182,11 +1162,6 @@ parameters:
count: 1 count: 1
path: src/Controller/TimesheetTeamController.php path: src/Controller/TimesheetTeamController.php
-
message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\<App\\\\Entity\\\\User\\>, iterable\\<App\\\\Entity\\\\User\\> given\\.$#"
count: 1
path: src/Controller/UserController.php
- -
message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#" message: "#^Parameter \\#2 \\$plainPassword of method Symfony\\\\Component\\\\PasswordHasher\\\\Hasher\\\\UserPasswordHasherInterface\\:\\:hashPassword\\(\\) expects string, string\\|null given\\.$#"
count: 1 count: 1
@@ -4822,26 +4797,11 @@ parameters:
count: 1 count: 1
path: src/Repository/UserRepository.php path: src/Repository/UserRepository.php
-
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:getHydratedResultsByQuery\\(\\) should return iterable\\<App\\\\Entity\\\\User\\> but returns mixed\\.$#"
count: 1
path: src/Repository/UserRepository.php
-
message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:saveUser\\(\\) has no return type specified\\.$#"
count: 1
path: src/Repository/UserRepository.php
- -
message: "#^Parameter \\#1 \\$identifier of method App\\\\Repository\\\\UserRepository\\:\\:loadUserByIdentifier\\(\\) expects string, mixed given\\.$#" message: "#^Parameter \\#1 \\$identifier of method App\\\\Repository\\\\UserRepository\\:\\:loadUserByIdentifier\\(\\) expects string, mixed given\\.$#"
count: 1 count: 1
path: src/Repository/UserRepository.php path: src/Repository/UserRepository.php
-
message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) expects array\\<App\\\\Entity\\\\User\\|int\\>, mixed given\\.$#"
count: 1
path: src/Repository/UserRepository.php
- -
message: "#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Security\\\\Core\\\\Exception\\\\AuthenticationException constructor expects string, string\\|null given\\.$#" message: "#^Parameter \\#1 \\$message of class Symfony\\\\Component\\\\Security\\\\Core\\\\Exception\\\\AuthenticationException constructor expects string, string\\|null given\\.$#"
count: 1 count: 1

View File

@@ -21,6 +21,7 @@ use App\Form\UserPreferencesForm;
use App\Form\UserRolesType; use App\Form\UserRolesType;
use App\Form\UserTeamsType; use App\Form\UserTeamsType;
use App\Form\UserTwoFactorType; use App\Form\UserTwoFactorType;
use App\Repository\Query\TimesheetStatisticQuery;
use App\Repository\TeamRepository; use App\Repository\TeamRepository;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use App\Repository\UserRepository; use App\Repository\UserRepository;
@@ -84,13 +85,15 @@ final class ProfileController extends AbstractController
// but we need a full year, because the chart needs always 12 month // but we need a full year, because the chart needs always 12 month
$begin = $dateFactory->createStartOfYear($begin); $begin = $dateFactory->createStartOfYear($begin);
$query = new TimesheetStatisticQuery($begin, $end, [$profile]);
$viewVars = [ $viewVars = [
'tab' => 'charts', 'tab' => 'charts',
'page_setup' => $this->getPageSetup($profile, 'charts'), 'page_setup' => $this->getPageSetup($profile, 'charts'),
'user' => $profile, 'user' => $profile,
'stats' => $userStats, 'stats' => $userStats,
'workingSince' => $workStartingDay, 'workingSince' => $workStartingDay,
'workMonths' => $statisticService->getMonthlyStats($begin, $end, [$profile])[0] 'workMonths' => $statisticService->getMonthlyStats($query)[0]
]; ];
return $this->render('user/stats.html.twig', $viewVars); return $this->render('user/stats.html.twig', $viewVars);

View File

@@ -15,6 +15,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter;
use App\Model\DailyStatistic; use App\Model\DailyStatistic;
use App\Reporting\MonthlyUserList\MonthlyUserList; use App\Reporting\MonthlyUserList\MonthlyUserList;
use App\Reporting\MonthlyUserList\MonthlyUserListForm; use App\Reporting\MonthlyUserList\MonthlyUserListForm;
use App\Repository\Query\TimesheetStatisticQuery;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Repository\Query\VisibilityInterface; use App\Repository\Query\VisibilityInterface;
use App\Repository\UserRepository; use App\Repository\UserRepository;
@@ -106,7 +107,9 @@ final class ReportUsersMonthController extends AbstractController
$hasData = true; $hasData = true;
if (!empty($allUsers)) { if (!empty($allUsers)) {
$dayStats = $statisticService->getDailyStatistics($start, $end, $allUsers); $statsQuery = new TimesheetStatisticQuery($start, $end, $allUsers);
$statsQuery->setProject($values->getProject());
$dayStats = $statisticService->getDailyStatistics($statsQuery);
} }
if (empty($dayStats)) { if (empty($dayStats)) {

View File

@@ -15,6 +15,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter;
use App\Model\DailyStatistic; use App\Model\DailyStatistic;
use App\Reporting\WeeklyUserList\WeeklyUserList; use App\Reporting\WeeklyUserList\WeeklyUserList;
use App\Reporting\WeeklyUserList\WeeklyUserListForm; use App\Reporting\WeeklyUserList\WeeklyUserListForm;
use App\Repository\Query\TimesheetStatisticQuery;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Repository\Query\VisibilityInterface; use App\Repository\Query\VisibilityInterface;
use App\Repository\UserRepository; use App\Repository\UserRepository;
@@ -102,7 +103,9 @@ final class ReportUsersWeekController extends AbstractController
$hasData = true; $hasData = true;
if (!empty($allUsers)) { if (!empty($allUsers)) {
$dayStats = $statisticService->getDailyStatistics($start, $end, $allUsers); $statsQuery = new TimesheetStatisticQuery($start, $end, $allUsers);
$statsQuery->setProject($values->getProject());
$dayStats = $statisticService->getDailyStatistics($statsQuery);
} }
if (empty($dayStats)) { if (empty($dayStats)) {

View File

@@ -16,6 +16,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter;
use App\Model\MonthlyStatistic; use App\Model\MonthlyStatistic;
use App\Reporting\YearlyUserList\YearlyUserList; use App\Reporting\YearlyUserList\YearlyUserList;
use App\Reporting\YearlyUserList\YearlyUserListForm; use App\Reporting\YearlyUserList\YearlyUserListForm;
use App\Repository\Query\TimesheetStatisticQuery;
use App\Repository\Query\UserQuery; use App\Repository\Query\UserQuery;
use App\Repository\Query\VisibilityInterface; use App\Repository\Query\VisibilityInterface;
use App\Repository\UserRepository; use App\Repository\UserRepository;
@@ -107,7 +108,9 @@ final class ReportUsersYearController extends AbstractController
$hasData = true; $hasData = true;
if (!empty($allUsers)) { if (!empty($allUsers)) {
$monthStats = $statisticService->getMonthlyStats($start, $end, $allUsers); $statsQuery = new TimesheetStatisticQuery($start, $end, $allUsers);
$statsQuery->setProject($values->getProject());
$monthStats = $statisticService->getMonthlyStats($statsQuery);
} }
if (empty($monthStats)) { if (empty($monthStats)) {

View File

@@ -9,6 +9,7 @@
namespace App\Reporting; namespace App\Reporting;
use App\Entity\Project;
use App\Entity\Team; use App\Entity\Team;
abstract class AbstractUserList abstract class AbstractUserList
@@ -17,6 +18,7 @@ abstract class AbstractUserList
private bool $decimal = false; private bool $decimal = false;
private string $sumType = 'duration'; private string $sumType = 'duration';
private ?Team $team = null; private ?Team $team = null;
private ?Project $project = null;
public function getDate(): ?\DateTimeInterface public function getDate(): ?\DateTimeInterface
{ {
@@ -61,4 +63,14 @@ abstract class AbstractUserList
{ {
$this->team = $team; $this->team = $team;
} }
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): void
{
$this->project = $project;
}
} }

View File

@@ -10,6 +10,7 @@
namespace App\Reporting\MonthlyUserList; namespace App\Reporting\MonthlyUserList;
use App\Form\Type\MonthPickerType; use App\Form\Type\MonthPickerType;
use App\Form\Type\ProjectType;
use App\Form\Type\ReportSumType; use App\Form\Type\ReportSumType;
use App\Form\Type\TeamType; use App\Form\Type\TeamType;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
@@ -33,6 +34,11 @@ final class MonthlyUserListForm extends AbstractType
'required' => false, 'required' => false,
'width' => false, 'width' => false,
]); ]);
$builder->add('project', ProjectType::class, [
'multiple' => false,
'required' => false,
'width' => false,
]);
$builder->add('sumType', ReportSumType::class); $builder->add('sumType', ReportSumType::class);
} }

View File

@@ -9,6 +9,7 @@
namespace App\Reporting\WeeklyUserList; namespace App\Reporting\WeeklyUserList;
use App\Form\Type\ProjectType;
use App\Form\Type\ReportSumType; use App\Form\Type\ReportSumType;
use App\Form\Type\TeamType; use App\Form\Type\TeamType;
use App\Form\Type\WeekPickerType; use App\Form\Type\WeekPickerType;
@@ -33,6 +34,11 @@ final class WeeklyUserListForm extends AbstractType
'required' => false, 'required' => false,
'width' => false, 'width' => false,
]); ]);
$builder->add('project', ProjectType::class, [
'multiple' => false,
'required' => false,
'width' => false,
]);
$builder->add('sumType', ReportSumType::class); $builder->add('sumType', ReportSumType::class);
} }

View File

@@ -9,6 +9,7 @@
namespace App\Reporting\YearlyUserList; namespace App\Reporting\YearlyUserList;
use App\Form\Type\ProjectType;
use App\Form\Type\ReportSumType; use App\Form\Type\ReportSumType;
use App\Form\Type\TeamType; use App\Form\Type\TeamType;
use App\Form\Type\YearPickerType; use App\Form\Type\YearPickerType;
@@ -34,6 +35,11 @@ final class YearlyUserListForm extends AbstractType
'required' => false, 'required' => false,
'width' => false, 'width' => false,
]); ]);
$builder->add('project', ProjectType::class, [
'multiple' => false,
'required' => false,
'width' => false,
]);
$builder->add('sumType', ReportSumType::class); $builder->add('sumType', ReportSumType::class);
} }

View File

@@ -0,0 +1,57 @@
<?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;
use App\Entity\Project;
use App\Entity\User;
final class TimesheetStatisticQuery
{
private ?Project $project = null;
/**
* @param array<User> $users
*/
public function __construct(
private readonly \DateTimeInterface $begin,
private readonly \DateTimeInterface $end,
private array $users
)
{
}
public function getBegin(): \DateTimeInterface
{
return $this->begin;
}
public function getEnd(): \DateTimeInterface
{
return $this->end;
}
/**
* @return User[]
*/
public function getUsers(): array
{
return $this->users;
}
public function getProject(): ?Project
{
return $this->project;
}
public function setProject(?Project $project): void
{
$this->project = $project;
}
}

View File

@@ -53,7 +53,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
* @throws ORMException * @throws ORMException
* @throws \Doctrine\ORM\OptimisticLockException * @throws \Doctrine\ORM\OptimisticLockException
*/ */
public function saveUser(User $user) public function saveUser(User $user): void
{ {
$entityManager = $this->getEntityManager(); $entityManager = $this->getEntityManager();
$entityManager->persist($user); $entityManager->persist($user);
@@ -397,7 +397,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
* @param UserQuery $query * @param UserQuery $query
* @return User[] * @return User[]
*/ */
public function getUsersForQuery(UserQuery $query): iterable public function getUsersForQuery(UserQuery $query): array
{ {
$qb = $this->getQueryBuilderForQuery($query); $qb = $this->getQueryBuilderForQuery($query);
@@ -408,8 +408,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us
* @param QueryBuilder $qb * @param QueryBuilder $qb
* @return User[] * @return User[]
*/ */
protected function getHydratedResultsByQuery(QueryBuilder $qb): iterable protected function getHydratedResultsByQuery(QueryBuilder $qb): array
{ {
/** @var array<User> $results */
$results = $qb->getQuery()->getResult(); $results = $qb->getQuery()->getResult();
$loader = new UserLoader($qb->getEntityManager()); $loader = new UserLoader($qb->getEntityManager());

View File

@@ -12,8 +12,8 @@ namespace App\Timesheet;
use App\Entity\User; use App\Entity\User;
use App\Model\DailyStatistic; use App\Model\DailyStatistic;
use App\Model\MonthlyStatistic; use App\Model\MonthlyStatistic;
use App\Repository\Query\TimesheetStatisticQuery;
use App\Repository\TimesheetRepository; use App\Repository\TimesheetRepository;
use DateTime;
use DateTimeInterface; use DateTimeInterface;
final class TimesheetStatisticService final class TimesheetStatisticService
@@ -23,13 +23,15 @@ final class TimesheetStatisticService
} }
/** /**
* @param DateTimeInterface $begin
* @param DateTimeInterface $end
* @param User[] $users
* @return DailyStatistic[] * @return DailyStatistic[]
*/ */
public function getDailyStatistics(DateTimeInterface $begin, DateTimeInterface $end, array $users): array public function getDailyStatistics(TimesheetStatisticQuery $query): array
{ {
$begin = $query->getBegin();
$end = $query->getEnd();
$users = $query->getUsers();
$project = $query->getProject();
/** @var DailyStatistic[] $stats */ /** @var DailyStatistic[] $stats */
$stats = []; $stats = [];
@@ -63,6 +65,13 @@ final class TimesheetStatisticService
->addGroupBy('billable') ->addGroupBy('billable')
; ;
if ($project !== null) {
$qb
->andWhere($qb->expr()->eq('t.project', ':project'))
->setParameter('project', $project)
;
}
$results = $qb->getQuery()->getResult(); $results = $qb->getQuery()->getResult();
foreach ($results as $row) { foreach ($results as $row) {
@@ -241,7 +250,7 @@ final class TimesheetStatisticService
return $stats; return $stats;
} }
public function findFirstRecordDate(User $user): ?DateTime public function findFirstRecordDate(User $user): ?\DateTimeImmutable
{ {
$result = $this->repository->createQueryBuilder('t') $result = $this->repository->createQueryBuilder('t')
->select('MIN(t.begin)') ->select('MIN(t.begin)')
@@ -254,19 +263,19 @@ final class TimesheetStatisticService
return null; return null;
} }
return new DateTime((string) $result, new \DateTimeZone($user->getTimezone())); return new \DateTimeImmutable((string) $result, new \DateTimeZone($user->getTimezone()));
} }
/** /**
* Returns an array of Year statistics.
*
* @param DateTime $begin
* @param DateTime $end
* @param User[] $users
* @return MonthlyStatistic[] * @return MonthlyStatistic[]
*/ */
public function getMonthlyStats(DateTime $begin, DateTime $end, array $users): array public function getMonthlyStats(TimesheetStatisticQuery $query): array
{ {
$begin = $query->getBegin();
$end = $query->getEnd();
$users = $query->getUsers();
$project = $query->getProject();
/** @var MonthlyStatistic[] $stats */ /** @var MonthlyStatistic[] $stats */
$stats = []; $stats = [];
@@ -297,6 +306,13 @@ final class TimesheetStatisticService
->addGroupBy('billable') ->addGroupBy('billable')
; ;
if ($project !== null) {
$qb
->andWhere($qb->expr()->eq('t.project', ':project'))
->setParameter('project', $project)
;
}
$results = $qb->getQuery()->getResult(); $results = $qb->getQuery()->getResult();
foreach ($results as $row) { foreach ($results as $row) {

View File

@@ -5,6 +5,9 @@
{% if form.team is defined %} {% if form.team is defined %}
{{ form_widget(form.team, {'label': false, 'placeholder': 'please_choose'}) }} {{ form_widget(form.team, {'label': false, 'placeholder': 'please_choose'}) }}
{% endif %} {% endif %}
{% if form.project is defined %}
{{ form_widget(form.project, {'label': false, 'placeholder': 'please_choose'}) }}
{% endif %}
{{ form_widget(form.sumType) }} {{ form_widget(form.sumType) }}
{% from '@theme/components/buttons.html.twig' import submit_button %} {% from '@theme/components/buttons.html.twig' import submit_button %}
{{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }} {{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }}