diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php new file mode 100644 index 000000000..ef38b7335 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingPeriodRegulationListController.php @@ -0,0 +1,167 @@ +accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository; + $this->engine = $engine; + $this->formFactory = $formFactory; + $this->paginatorFactory = $paginatorFactory; + $this->security = $security; + $this->translatableStringHelper = $translatableStringHelper; + } + + /** + * @Route("/{_locale}/person/periods/undispatched", name="chill_person_course_list_regulation") + */ + public function listRegul(Request $request): Response + { + if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) { + throw new AccessDeniedHttpException(); + } + + $form = $this->buildFilterForm(); + + $form->handleRequest($request); + + $total = $this->accompanyingPeriodACLAwareRepository->countByUnDispatched( + $form['jobs']->getData(), + $form['services']->getData(), + $form['locations']->getData(), + ); + $paginator = $this->paginatorFactory->create($total); + $periods = $this->accompanyingPeriodACLAwareRepository + ->findByUnDispatched( + $form['jobs']->getData(), + $form['services']->getData(), + $form['locations']->getData(), + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + return new Response( + $this->engine->render('@ChillPerson/AccompanyingCourse/dispatch_list.html.twig', [ + 'paginator' => $paginator, + 'periods' => $periods, + 'form' => $form->createView(), + ]) + ); + } + + private function buildFilterForm(): FormInterface + { + $data = [ + 'services' => [], + 'jobs' => [], + 'locations' => [], + ]; + + $builder = $this->formFactory->createBuilder(FormType::class, $data, [ + 'method' => 'get', 'csrf_protection' => false, ]); + + $builder + ->add('services', EntityType::class, [ + 'class' => Scope::class, + 'query_builder' => static function (EntityRepository $er) { + return $er->createQueryBuilder('s'); + }, + 'choice_label' => function (Scope $s) { + return $this->translatableStringHelper->localize($s->getName()); + }, + 'multiple' => true, + 'label' => 'Service', + 'required' => false, + ]) + ->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'query_builder' => static function (EntityRepository $er) { + $qb = $er->createQueryBuilder('j'); + $qb->andWhere($qb->expr()->eq('j.active', "'TRUE'")); + + return $qb; + }, + 'choice_label' => function (UserJob $j) { + return $this->translatableStringHelper->localize($j->getLabel()); + }, + 'multiple' => true, + 'label' => 'Métier', + 'required' => false, + ]) + ->add('locations', EntityType::class, [ + 'class' => Location::class, + 'query_builder' => static function (EntityRepository $er) { + $qb = $er->createQueryBuilder('l'); + $qb + ->join('l.locationType', 't') + ->where( + $qb->expr()->andX( + $qb->expr()->eq('t.availableForUsers', "'TRUE'"), + $qb->expr()->eq('t.active', "'TRUE'"), + $qb->expr()->eq('l.active', "'TRUE'"), + $qb->expr()->eq('l.availableForUsers', "'TRUE'") + ) + ); + + return $qb; + }, + 'choice_label' => static function (Location $l) { + return $l->getName(); + }, + 'multiple' => true, + 'group_by' => function (Location $l) { + if (null === $type = $l->getLocationType()) { + return null; + } + + return $this->translatableStringHelper->localize($type->getTitle()); + }, + 'label' => 'Localisation administrative', + 'required' => false, + ]); + + return $builder->getForm(); + } +} diff --git a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php index 722598ef7..183d2353e 100644 --- a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php @@ -11,12 +11,13 @@ declare(strict_types=1); namespace Chill\PersonBundle\Menu; +use Chill\MainBundle\Entity\User; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Knp\Menu\MenuItem; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -24,20 +25,20 @@ use Symfony\Contracts\Translation\TranslatorInterface; */ class SectionMenuBuilder implements LocalMenuBuilderInterface { - protected AuthorizationCheckerInterface $authorizationChecker; - protected ParameterBagInterface $parameterBag; protected TranslatorInterface $translator; + private Security $security; + /** * SectionMenuBuilder constructor. */ - public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator, ParameterBagInterface $parameterBag) + public function __construct(ParameterBagInterface $parameterBag, Security $security, TranslatorInterface $translator) { - $this->authorizationChecker = $authorizationChecker; - $this->translator = $translator; $this->parameterBag = $parameterBag; + $this->security = $security; + $this->translator = $translator; } /** @@ -45,7 +46,7 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface */ public function buildMenu($menuId, MenuItem $menu, array $parameters) { - if ($this->authorizationChecker->isGranted(PersonVoter::CREATE) && $this->parameterBag->get('chill_person.create_person_allowed')) { + if ($this->security->isGranted(PersonVoter::CREATE) && $this->parameterBag->get('chill_person.create_person_allowed')) { $menu->addChild($this->translator->trans('Add a person'), [ 'route' => 'chill_person_new', ]) @@ -65,7 +66,7 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface ]); } - if ($this->authorizationChecker->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK, null)) { + if ($this->security->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK, null)) { $menu->addChild($this->translator->trans('reassign.Bulk reassign'), [ 'route' => 'chill_course_list_reassign', ]) @@ -74,6 +75,16 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface 'icons' => [], ]); } + + if ($this->security->getUser() instanceof User && $this->security->isGranted('ROLE_USER')) { + $menu + ->addChild('Régulation', [ + 'route' => 'chill_person_course_list_regulation', + ]) + ->setExtras([ + 'order' => 150, + ]); + } } public static function getMenuIds(): array diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index 8efd60e58..653fed9c2 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -11,13 +11,19 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; +use Chill\MainBundle\Entity\Location; +use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use DateTime; +use Doctrine\ORM\QueryBuilder; use Symfony\Component\Security\Core\Security; use function count; @@ -62,6 +68,15 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb; } + public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int + { + $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + + $qb->select('COUNT(ap)'); + + return $qb->getQuery()->getSingleScalarResult(); + } + public function countByUserOpenedAccompanyingPeriod(?User $user): int { if (null === $user) { @@ -126,6 +141,23 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb->getQuery()->getResult(); } + public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array + { + $qb = $this->addACLByUnDispatched($this->buildQueryUnDispatched($jobs, $services, $administrativeLocations)); + + $qb->select('ap'); + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + return $qb->getQuery()->getResult(); + } + /** * @return array|AccompanyingPeriod[] */ @@ -146,4 +178,88 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb->getQuery()->getResult(); } + + private function addACLByUnDispatched(QueryBuilder $qb): QueryBuilder + { + $centers = $this->authorizationHelper->getReachableCenters( + $this->security->getUser(), + AccompanyingPeriodVoter::SEE + ); + + $orX = $qb->expr()->orX(); + + if (0 === count($centers)) { + return $qb->andWhere("'FALSE' = 'TRUE'"); + } + + foreach ($centers as $key => $center) { + $scopes = $this->authorizationHelper + ->getReachableCircles( + $this->security->getUser(), + AccompanyingPeriodVoter::SEE, + $center + ); + + $and = $qb->expr()->andX( + $qb->expr()->exists('SELECT part FROM ' . AccompanyingPeriodParticipation::class . ' part ' . + "JOIN part.person p WHERE part.accompanyingPeriod = ap.id AND p.center = :center_{$key}") + ); + $qb->setParameter('center_' . $key, $center); + $orScope = $qb->expr()->orX(); + + foreach ($scopes as $skey => $scope) { + $orScope->add( + $qb->expr()->isMemberOf(':scope_' . $key . '_' . $skey, 'ap.scopes') + ); + $qb->setParameter('scope_' . $key . '_' . $skey, $scope); + } + + $and->add($orScope); + $orX->add($and); + } + + return $qb->andWhere($orX); + } + + /** + * @param array|UserJob[] $jobs + * @param array|Scope[] $services + * @param array|Location[] $locations + */ + private function buildQueryUnDispatched(array $jobs, array $services, array $locations): QueryBuilder + { + $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); + + $qb->where( + $qb->expr()->andX( + $qb->expr()->isNull('ap.user'), + $qb->expr()->neq('ap.step', ':draft'), + $qb->expr()->neq('ap.step', ':closed') + ) + ) + ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT) + ->setParameter('closed', AccompanyingPeriod::STEP_CLOSED); + + if (0 < count($jobs)) { + $qb->andWhere($qb->expr()->in('ap.job', ':jobs')) + ->setParameter('jobs', $jobs); + } + + if (0 < count($locations)) { + $qb->andWhere($qb->expr()->in('ap.administrativeLocation', ':locations')) + ->setParameter('locations', $locations); + } + + if (0 < count($services)) { + $or = $qb->expr()->orX(); + + foreach ($services as $key => $service) { + $or->add($qb->expr()->isMemberOf('ap.scopes', ':scope_' . $key)); + $qb->setParameter('scope_' . $key, $service); + } + $qb->andWhere($or); + } + + return $qb; + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index a02ec27c6..e8d0bd856 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -11,11 +11,20 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; +use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserJob; +use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\Person; interface AccompanyingPeriodACLAwareRepositoryInterface { + /** + * @param array|UserJob[] $jobs + * @param array|Scope[] $services + */ + public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int; + public function countByUserOpenedAccompanyingPeriod(?User $user): int; public function findByPerson( @@ -26,5 +35,13 @@ interface AccompanyingPeriodACLAwareRepositoryInterface ?int $offset = null ): array; + /** + * @param array|UserJob[] $jobs if empty, does not take this argument into account + * @param array|Scope[] $services if empty, does not take this argument into account + * + * @return array|AccompanyingPeriod[] + */ + public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; + public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig new file mode 100644 index 000000000..9b48883ef --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig @@ -0,0 +1,88 @@ +{% extends 'ChillMainBundle::layout.html.twig' %} + +{% block title "Liste de parcours à répartir" %} + +{% block js %} + {{ encore_entry_script_tags('mod_set_referrer') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_set_referrer') }} +{% endblock %} + +{% macro period_meta(period) %} + {% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE', period) %} +
+ {% endif %} +{% endmacro %} + +{% macro period_actions(period) %} + {% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %} +Aucun parcours à désigner, ou droits insuffisants pour les afficher
+ {% else %} + +{{ paginator.totalItems }} parcours à attribuer (calculé ce jour à {{ null|format_time('medium') }})
+ +