diff --git a/.changes/unreleased/Feature-20230706-134010.yaml b/.changes/unreleased/Feature-20230706-134010.yaml new file mode 100644 index 000000000..73a0727fc --- /dev/null +++ b/.changes/unreleased/Feature-20230706-134010.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: Adding OrderFilter to the list of social actions. +time: 2023-07-06T13:40:10.339001208+02:00 +custom: + Issue: "120" diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php index 74d9a7a22..8bcc7ffae 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelper.php @@ -14,19 +14,11 @@ namespace Chill\MainBundle\Templating\Listing; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use DateTimeImmutable; -use Symfony\Component\Form\Extension\Core\Type\FormType; -use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\PropertyAccess\PropertyAccessor; -use Symfony\Component\PropertyAccess\PropertyAccessorInterface; -use Symfony\Component\PropertyAccess\PropertyPath; -use Symfony\Component\PropertyAccess\PropertyPathInterface; -use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; -use function count; final class FilterOrderHelper { @@ -54,7 +46,6 @@ final class FilterOrderHelper */ private array $entityChoices = []; - /** * @var array */ diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php index 8d15ca30f..66d020ad1 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseWorkController.php @@ -11,45 +11,39 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; +use Chill\MainBundle\Entity\UserJob; use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\MainBundle\Templating\Listing\FilterOrderHelper; +use Chill\MainBundle\Templating\Listing\FilterOrderHelperFactoryInterface; +use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Contracts\Translation\TranslatorInterface; -class AccompanyingCourseWorkController extends AbstractController +final class AccompanyingCourseWorkController extends AbstractController { - private LoggerInterface $chillLogger; - - private PaginatorFactory $paginator; - - private SerializerInterface $serializer; - - private TranslatorInterface $trans; - - private AccompanyingPeriodWorkRepository $workRepository; - public function __construct( - TranslatorInterface $trans, - SerializerInterface $serializer, - AccompanyingPeriodWorkRepository $workRepository, - PaginatorFactory $paginator, - LoggerInterface $chillLogger + private readonly TranslatorInterface $trans, + private readonly SerializerInterface $serializer, + private readonly AccompanyingPeriodWorkRepository $workRepository, + private readonly PaginatorFactory $paginator, + private readonly LoggerInterface $chillLogger, + private readonly TranslatableStringHelperInterface $translatableStringHelper, + private readonly FilterOrderHelperFactoryInterface $filterOrderHelperFactory ) { - $this->trans = $trans; - $this->serializer = $serializer; - $this->workRepository = $workRepository; - $this->paginator = $paginator; - $this->chillLogger = $chillLogger; } /** @@ -162,11 +156,21 @@ class AccompanyingCourseWorkController extends AbstractController { $this->denyAccessUnlessGranted(AccompanyingPeriodWorkVoter::SEE, $period); + $filter = $this->buildFilterOrder($period); + + $filterData = [ + 'types' => $filter->hasEntityChoice('typesFilter') ? $filter->getEntityChoiceData('typesFilter') : [], + 'before' => $filter->getDateRangeData('dateFilter')['to'], + 'after' => $filter->getDateRangeData('dateFilter')['from'], + 'user' => $filter->getUserPickerData('userFilter') + ]; + $totalItems = $this->workRepository->countByAccompanyingPeriod($period); $paginator = $this->paginator->create($totalItems); $works = $this->workRepository->findByAccompanyingPeriodOpenFirst( $period, + $filterData, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() ); @@ -175,6 +179,7 @@ class AccompanyingCourseWorkController extends AbstractController 'accompanyingCourse' => $period, 'works' => $works, 'paginator' => $paginator, + 'filter' => $filter ]); } @@ -199,7 +204,7 @@ class AccompanyingCourseWorkController extends AbstractController ]); } - private function createDeleteForm(int $id): Form + private function createDeleteForm(int $id): FormInterface { $params = []; $params['id'] = $id; @@ -210,4 +215,26 @@ class AccompanyingCourseWorkController extends AbstractController ->add('submit', SubmitType::class, ['label' => 'Delete']) ->getForm(); } + + private function buildFilterOrder($associatedPeriod): FilterOrderHelper + { + $filterBuilder = $this->filterOrderHelperFactory->create(self::class); + $types = $this->workRepository->findActionTypeByPeriod($associatedPeriod); + + $filterBuilder + ->addDateRange('dateFilter', 'accompanying_course_work.date_filter'); + + if (1 < count($types)) { + $filterBuilder + ->addEntityChoice('typesFilter', 'accompanying_course_work.types_filter', \Chill\PersonBundle\Entity\SocialWork\SocialAction::class, $types, [ + 'choice_label' => fn (SocialAction $sa) => $this->translatableStringHelper->localize($sa->getTitle()) + ]); + } + + $filterBuilder + ->addUserPicker('userFilter', 'accompanying_course_work.user_filter', ['required' => false]) + ; + + return $filterBuilder->build(); + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php index bf2d34aae..56365452a 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkRepository.php @@ -95,29 +95,103 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository * * then, closed works * * @return AccompanyingPeriodWork[] + * @param array{types?: list, user?: list, after?: null|\DateTimeImmutable, before?: null|\DateTimeImmutable} $filters */ - public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, int $limit = 10, int $offset = 0): array + public function findByAccompanyingPeriodOpenFirst(AccompanyingPeriod $period, array $filters, int $limit = 10, int $offset = 0): array { $rsm = new ResultSetMappingBuilder($this->em); $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w - WHERE accompanyingPeriod_id = :periodId - ORDER BY - CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, - startdate DESC, - enddate DESC, - id DESC - LIMIT :limit OFFSET :offset"; + LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id + WHERE accompanyingPeriod_id = :periodId"; + + // implement filters + + if ([] !== ($filters['types'] ?? [])) { + $sql .= " AND w.socialaction_id IN (:types)"; + } + + if ([] !== ($filters['user'] ?? [])) { + $sql .= " AND rw.user_id IN (" + . implode( + ', ', + // we add a user_xx for each key of the 'user' list + array_map(fn (User $u, int $idx) => ':user_' . $idx, $filters['user'], array_keys($filters['user'])) + ) + . ")"; + } + + $sql .= " AND daterange(:after::date, :before::date) && daterange(w.startDate, w.endDate)"; + + // if the start and end date were inversed, we inverse the order to avoid an error + if (null !== ($filters['after'] ?? null) && null !== ($filters['before']) && $filters['after'] > $filters['before']) { + $before = $filters['after']; + $after = $filters['before']; + } else { + $before = $filters['before']; + $after = $filters['after']; + } + + // set limit and offset + $sql .= " ORDER BY + CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC, + startdate DESC, + enddate DESC, + id DESC"; + + $sql .= " LIMIT :limit OFFSET :offset"; + + $typeIds = []; + foreach ($filters['types'] as $type) { + $typeIds[] = $type->getId(); + } $nq = $this->em->createNativeQuery($sql, $rsm) ->setParameter('periodId', $period->getId(), Types::INTEGER) + ->setParameter('types', $typeIds) + ->setParameter('after', $after) + ->setParameter('before', $before) ->setParameter('limit', $limit, Types::INTEGER) ->setParameter('offset', $offset, Types::INTEGER); + foreach ($filters['user'] as $key => $user) { + $nq->setParameter('user_' . $key, $user); + } + return $nq->getResult(); } + /** + * Return a list of types of social actions associated to the accompanying period + * + * @return array + */ + public function findActionTypeByPeriod(AccompanyingPeriod $period): array + { + $in = $this->em->createQueryBuilder(); + $in + ->select('1') + ->from(AccompanyingPeriodWork::class, 'apw'); + + + $in->andWhere('apw.accompanyingPeriod = :period')->setParameter('period', $period); + + + // join between the embedded exist query and the main query + $in->andWhere('apw.socialAction = sa'); + + $qb = $this->em->createQueryBuilder()->setParameters($in->getParameters()); + $qb + ->select('sa') + ->from(SocialAction::class, 'sa') + ->where( + $qb->expr()->exists($in->getDQL()) + ); + + return $qb->getQuery()->getResult(); + } + public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array { return $this->buildQueryNearEndDateByUser($user, $since, $until) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig index ab1989f63..1cedfa694 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig @@ -5,18 +5,23 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }} + {{ encore_entry_script_tags('mod_pickentity_type') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }} + {{ encore_entry_link_tags('mod_pickentity_type') }} {% endblock %} + {% block content %}

{{ block('title') }}

+ {{ filter|chill_render_filter_order_helper }} + {% if works|length == 0 %}

{{ 'accompanying_course_work.Any work'|trans }}

{% else %} diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 809d082b4..892bb0dfd 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -913,6 +913,9 @@ accompanying_course_work: social_evaluation: Évaluation private_comment: Commentaire privé timeSpent: Temps de rédaction + date_filter: Filtrer par date + types_filter: Filtrer par type d'action + user_filter: Filtrer par intervenant #