From 7abe7877785782c01d123624802ac242e0b18835 Mon Sep 17 00:00:00 2001 From: Kevin Papst Date: Wed, 7 Feb 2024 18:00:29 +0100 Subject: [PATCH] added project filter in user-list reports (#4615) --- phpstan.neon | 40 ------------- src/Controller/ProfileController.php | 5 +- .../Reporting/ReportUsersMonthController.php | 5 +- .../Reporting/ReportUsersWeekController.php | 5 +- .../Reporting/ReportUsersYearController.php | 5 +- src/Reporting/AbstractUserList.php | 12 ++++ .../MonthlyUserList/MonthlyUserListForm.php | 6 ++ .../WeeklyUserList/WeeklyUserListForm.php | 6 ++ .../YearlyUserList/YearlyUserListForm.php | 6 ++ .../Query/TimesheetStatisticQuery.php | 57 +++++++++++++++++++ src/Repository/UserRepository.php | 7 ++- src/Timesheet/TimesheetStatisticService.php | 42 +++++++++----- .../report_user_list_layout.html.twig | 3 + 13 files changed, 139 insertions(+), 60 deletions(-) create mode 100644 src/Repository/Query/TimesheetStatisticQuery.php diff --git a/phpstan.neon b/phpstan.neon index e357de1b..023d9b34 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1032,11 +1032,6 @@ parameters: count: 1 path: src/Controller/Reporting/CustomerMonthlyProjectsController.php - - - message: "#^Parameter \\#3 \\$users of method App\\\\Reporting\\\\CustomerMonthlyProjects\\\\CustomerMonthlyProjectsRepository\\:\\:getGroupedByCustomerProjectActivityUser\\(\\) expects array\\, iterable\\ given\\.$#" - count: 1 - path: src/Controller/Reporting/CustomerMonthlyProjectsController.php - - message: "#^Cannot call method getId\\(\\) on App\\\\Entity\\\\Customer\\|null\\.$#" count: 3 @@ -1052,31 +1047,16 @@ parameters: count: 1 path: src/Controller/Reporting/ReportUsersMonthController.php - - - message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getDailyStatistics\\(\\) expects array\\, iterable\\ 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\\.$#" count: 1 path: src/Controller/Reporting/ReportUsersWeekController.php - - - message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getDailyStatistics\\(\\) expects array\\, iterable\\ 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\\.$#" count: 1 path: src/Controller/Reporting/ReportUsersYearController.php - - - message: "#^Parameter \\#3 \\$users of method App\\\\Timesheet\\\\TimesheetStatisticService\\:\\:getMonthlyStats\\(\\) expects array\\, iterable\\ 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\\.$#" count: 1 @@ -1182,11 +1162,6 @@ parameters: count: 1 path: src/Controller/TimesheetTeamController.php - - - message: "#^Parameter \\#1 \\$entries of method App\\\\Export\\\\Spreadsheet\\\\UserExporter\\:\\:export\\(\\) expects array\\, iterable\\ 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\\.$#" count: 1 @@ -4822,26 +4797,11 @@ parameters: count: 1 path: src/Repository/UserRepository.php - - - message: "#^Method App\\\\Repository\\\\UserRepository\\:\\:getHydratedResultsByQuery\\(\\) should return iterable\\ 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\\.$#" count: 1 path: src/Repository/UserRepository.php - - - message: "#^Parameter \\#1 \\$results of method App\\\\Repository\\\\Loader\\\\UserLoader\\:\\:loadResults\\(\\) expects array\\, 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\\.$#" count: 1 diff --git a/src/Controller/ProfileController.php b/src/Controller/ProfileController.php index eefc7057..36b66e00 100644 --- a/src/Controller/ProfileController.php +++ b/src/Controller/ProfileController.php @@ -21,6 +21,7 @@ use App\Form\UserPreferencesForm; use App\Form\UserRolesType; use App\Form\UserTeamsType; use App\Form\UserTwoFactorType; +use App\Repository\Query\TimesheetStatisticQuery; use App\Repository\TeamRepository; use App\Repository\TimesheetRepository; 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 $begin = $dateFactory->createStartOfYear($begin); + $query = new TimesheetStatisticQuery($begin, $end, [$profile]); + $viewVars = [ 'tab' => 'charts', 'page_setup' => $this->getPageSetup($profile, 'charts'), 'user' => $profile, 'stats' => $userStats, 'workingSince' => $workStartingDay, - 'workMonths' => $statisticService->getMonthlyStats($begin, $end, [$profile])[0] + 'workMonths' => $statisticService->getMonthlyStats($query)[0] ]; return $this->render('user/stats.html.twig', $viewVars); diff --git a/src/Controller/Reporting/ReportUsersMonthController.php b/src/Controller/Reporting/ReportUsersMonthController.php index 56992faa..816bf055 100644 --- a/src/Controller/Reporting/ReportUsersMonthController.php +++ b/src/Controller/Reporting/ReportUsersMonthController.php @@ -15,6 +15,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter; use App\Model\DailyStatistic; use App\Reporting\MonthlyUserList\MonthlyUserList; use App\Reporting\MonthlyUserList\MonthlyUserListForm; +use App\Repository\Query\TimesheetStatisticQuery; use App\Repository\Query\UserQuery; use App\Repository\Query\VisibilityInterface; use App\Repository\UserRepository; @@ -106,7 +107,9 @@ final class ReportUsersMonthController extends AbstractController $hasData = true; 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)) { diff --git a/src/Controller/Reporting/ReportUsersWeekController.php b/src/Controller/Reporting/ReportUsersWeekController.php index f0d812b4..90af4538 100644 --- a/src/Controller/Reporting/ReportUsersWeekController.php +++ b/src/Controller/Reporting/ReportUsersWeekController.php @@ -15,6 +15,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter; use App\Model\DailyStatistic; use App\Reporting\WeeklyUserList\WeeklyUserList; use App\Reporting\WeeklyUserList\WeeklyUserListForm; +use App\Repository\Query\TimesheetStatisticQuery; use App\Repository\Query\UserQuery; use App\Repository\Query\VisibilityInterface; use App\Repository\UserRepository; @@ -102,7 +103,9 @@ final class ReportUsersWeekController extends AbstractController $hasData = true; 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)) { diff --git a/src/Controller/Reporting/ReportUsersYearController.php b/src/Controller/Reporting/ReportUsersYearController.php index 32bb6e86..7b7cd531 100644 --- a/src/Controller/Reporting/ReportUsersYearController.php +++ b/src/Controller/Reporting/ReportUsersYearController.php @@ -16,6 +16,7 @@ use App\Export\Spreadsheet\Writer\XlsxWriter; use App\Model\MonthlyStatistic; use App\Reporting\YearlyUserList\YearlyUserList; use App\Reporting\YearlyUserList\YearlyUserListForm; +use App\Repository\Query\TimesheetStatisticQuery; use App\Repository\Query\UserQuery; use App\Repository\Query\VisibilityInterface; use App\Repository\UserRepository; @@ -107,7 +108,9 @@ final class ReportUsersYearController extends AbstractController $hasData = true; 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)) { diff --git a/src/Reporting/AbstractUserList.php b/src/Reporting/AbstractUserList.php index 8e32b8de..6e781548 100644 --- a/src/Reporting/AbstractUserList.php +++ b/src/Reporting/AbstractUserList.php @@ -9,6 +9,7 @@ namespace App\Reporting; +use App\Entity\Project; use App\Entity\Team; abstract class AbstractUserList @@ -17,6 +18,7 @@ abstract class AbstractUserList private bool $decimal = false; private string $sumType = 'duration'; private ?Team $team = null; + private ?Project $project = null; public function getDate(): ?\DateTimeInterface { @@ -61,4 +63,14 @@ abstract class AbstractUserList { $this->team = $team; } + + public function getProject(): ?Project + { + return $this->project; + } + + public function setProject(?Project $project): void + { + $this->project = $project; + } } diff --git a/src/Reporting/MonthlyUserList/MonthlyUserListForm.php b/src/Reporting/MonthlyUserList/MonthlyUserListForm.php index 684378d5..23aba050 100644 --- a/src/Reporting/MonthlyUserList/MonthlyUserListForm.php +++ b/src/Reporting/MonthlyUserList/MonthlyUserListForm.php @@ -10,6 +10,7 @@ namespace App\Reporting\MonthlyUserList; use App\Form\Type\MonthPickerType; +use App\Form\Type\ProjectType; use App\Form\Type\ReportSumType; use App\Form\Type\TeamType; use Symfony\Component\Form\AbstractType; @@ -33,6 +34,11 @@ final class MonthlyUserListForm extends AbstractType 'required' => false, 'width' => false, ]); + $builder->add('project', ProjectType::class, [ + 'multiple' => false, + 'required' => false, + 'width' => false, + ]); $builder->add('sumType', ReportSumType::class); } diff --git a/src/Reporting/WeeklyUserList/WeeklyUserListForm.php b/src/Reporting/WeeklyUserList/WeeklyUserListForm.php index 62ae6acf..d8af6f6a 100644 --- a/src/Reporting/WeeklyUserList/WeeklyUserListForm.php +++ b/src/Reporting/WeeklyUserList/WeeklyUserListForm.php @@ -9,6 +9,7 @@ namespace App\Reporting\WeeklyUserList; +use App\Form\Type\ProjectType; use App\Form\Type\ReportSumType; use App\Form\Type\TeamType; use App\Form\Type\WeekPickerType; @@ -33,6 +34,11 @@ final class WeeklyUserListForm extends AbstractType 'required' => false, 'width' => false, ]); + $builder->add('project', ProjectType::class, [ + 'multiple' => false, + 'required' => false, + 'width' => false, + ]); $builder->add('sumType', ReportSumType::class); } diff --git a/src/Reporting/YearlyUserList/YearlyUserListForm.php b/src/Reporting/YearlyUserList/YearlyUserListForm.php index 3be0710c..7369824a 100644 --- a/src/Reporting/YearlyUserList/YearlyUserListForm.php +++ b/src/Reporting/YearlyUserList/YearlyUserListForm.php @@ -9,6 +9,7 @@ namespace App\Reporting\YearlyUserList; +use App\Form\Type\ProjectType; use App\Form\Type\ReportSumType; use App\Form\Type\TeamType; use App\Form\Type\YearPickerType; @@ -34,6 +35,11 @@ final class YearlyUserListForm extends AbstractType 'required' => false, 'width' => false, ]); + $builder->add('project', ProjectType::class, [ + 'multiple' => false, + 'required' => false, + 'width' => false, + ]); $builder->add('sumType', ReportSumType::class); } diff --git a/src/Repository/Query/TimesheetStatisticQuery.php b/src/Repository/Query/TimesheetStatisticQuery.php new file mode 100644 index 00000000..95ebd4a7 --- /dev/null +++ b/src/Repository/Query/TimesheetStatisticQuery.php @@ -0,0 +1,57 @@ + $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; + } +} diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index f4f1f3a2..9789ba98 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -53,7 +53,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us * @throws ORMException * @throws \Doctrine\ORM\OptimisticLockException */ - public function saveUser(User $user) + public function saveUser(User $user): void { $entityManager = $this->getEntityManager(); $entityManager->persist($user); @@ -397,7 +397,7 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us * @param UserQuery $query * @return User[] */ - public function getUsersForQuery(UserQuery $query): iterable + public function getUsersForQuery(UserQuery $query): array { $qb = $this->getQueryBuilderForQuery($query); @@ -408,8 +408,9 @@ class UserRepository extends EntityRepository implements UserLoaderInterface, Us * @param QueryBuilder $qb * @return User[] */ - protected function getHydratedResultsByQuery(QueryBuilder $qb): iterable + protected function getHydratedResultsByQuery(QueryBuilder $qb): array { + /** @var array $results */ $results = $qb->getQuery()->getResult(); $loader = new UserLoader($qb->getEntityManager()); diff --git a/src/Timesheet/TimesheetStatisticService.php b/src/Timesheet/TimesheetStatisticService.php index 837aefdf..a4e28797 100644 --- a/src/Timesheet/TimesheetStatisticService.php +++ b/src/Timesheet/TimesheetStatisticService.php @@ -12,8 +12,8 @@ namespace App\Timesheet; use App\Entity\User; use App\Model\DailyStatistic; use App\Model\MonthlyStatistic; +use App\Repository\Query\TimesheetStatisticQuery; use App\Repository\TimesheetRepository; -use DateTime; use DateTimeInterface; final class TimesheetStatisticService @@ -23,13 +23,15 @@ final class TimesheetStatisticService } /** - * @param DateTimeInterface $begin - * @param DateTimeInterface $end - * @param User[] $users * @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 */ $stats = []; @@ -63,6 +65,13 @@ final class TimesheetStatisticService ->addGroupBy('billable') ; + if ($project !== null) { + $qb + ->andWhere($qb->expr()->eq('t.project', ':project')) + ->setParameter('project', $project) + ; + } + $results = $qb->getQuery()->getResult(); foreach ($results as $row) { @@ -241,7 +250,7 @@ final class TimesheetStatisticService return $stats; } - public function findFirstRecordDate(User $user): ?DateTime + public function findFirstRecordDate(User $user): ?\DateTimeImmutable { $result = $this->repository->createQueryBuilder('t') ->select('MIN(t.begin)') @@ -254,19 +263,19 @@ final class TimesheetStatisticService 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[] */ - 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 */ $stats = []; @@ -297,6 +306,13 @@ final class TimesheetStatisticService ->addGroupBy('billable') ; + if ($project !== null) { + $qb + ->andWhere($qb->expr()->eq('t.project', ':project')) + ->setParameter('project', $project) + ; + } + $results = $qb->getQuery()->getResult(); foreach ($results as $row) { diff --git a/templates/reporting/report_user_list_layout.html.twig b/templates/reporting/report_user_list_layout.html.twig index b669e2ab..b4306ad5 100644 --- a/templates/reporting/report_user_list_layout.html.twig +++ b/templates/reporting/report_user_list_layout.html.twig @@ -5,6 +5,9 @@ {% if form.team is defined %} {{ form_widget(form.team, {'label': false, 'placeholder': 'please_choose'}) }} {% endif %} + {% if form.project is defined %} + {{ form_widget(form.project, {'label': false, 'placeholder': 'please_choose'}) }} + {% endif %} {{ form_widget(form.sumType) }} {% from '@theme/components/buttons.html.twig' import submit_button %} {{ submit_button('download', {'attr': {'formaction': path(export_route)}, 'icon': 'download', 'combined': false}, 'primary') }}