Merge branch 'issue120_filter_social_actions' into 'master'

Add filter to social actions

See merge request Chill-Projet/chill-bundles!570
This commit is contained in:
Julien Fastré 2023-07-14 09:14:21 +00:00
commit e6da727a11
6 changed files with 144 additions and 39 deletions

View File

@ -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"

View File

@ -14,19 +14,11 @@ namespace Chill\MainBundle\Templating\Listing;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\Listing\FilterOrderType; use Chill\MainBundle\Form\Type\Listing\FilterOrderType;
use DateTimeImmutable; 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\FormFactoryInterface;
use Symfony\Component\Form\FormInterface; use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\RequestStack; 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 array_merge;
use function count;
final class FilterOrderHelper final class FilterOrderHelper
{ {
@ -54,7 +46,6 @@ final class FilterOrderHelper
*/ */
private array $entityChoices = []; private array $entityChoices = [];
/** /**
* @var array<string, array{label: string, options: array}> * @var array<string, array{label: string, options: array}>
*/ */

View File

@ -11,45 +11,39 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\UserJob;
use Chill\MainBundle\Pagination\PaginatorFactory; 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;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; 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\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType; use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Form; use Symfony\Component\Form\Form;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Contracts\Translation\TranslatorInterface; 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( public function __construct(
TranslatorInterface $trans, private readonly TranslatorInterface $trans,
SerializerInterface $serializer, private readonly SerializerInterface $serializer,
AccompanyingPeriodWorkRepository $workRepository, private readonly AccompanyingPeriodWorkRepository $workRepository,
PaginatorFactory $paginator, private readonly PaginatorFactory $paginator,
LoggerInterface $chillLogger 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); $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); $totalItems = $this->workRepository->countByAccompanyingPeriod($period);
$paginator = $this->paginator->create($totalItems); $paginator = $this->paginator->create($totalItems);
$works = $this->workRepository->findByAccompanyingPeriodOpenFirst( $works = $this->workRepository->findByAccompanyingPeriodOpenFirst(
$period, $period,
$filterData,
$paginator->getItemsPerPage(), $paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber() $paginator->getCurrentPageFirstItemNumber()
); );
@ -175,6 +179,7 @@ class AccompanyingCourseWorkController extends AbstractController
'accompanyingCourse' => $period, 'accompanyingCourse' => $period,
'works' => $works, 'works' => $works,
'paginator' => $paginator, '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 = [];
$params['id'] = $id; $params['id'] = $id;
@ -210,4 +215,26 @@ class AccompanyingCourseWorkController extends AbstractController
->add('submit', SubmitType::class, ['label' => 'Delete']) ->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm(); ->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();
}
} }

View File

@ -95,29 +95,103 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
* * then, closed works * * then, closed works
* *
* @return AccompanyingPeriodWork[] * @return AccompanyingPeriodWork[]
* @param array{types?: list<SocialAction>, user?: list<User>, 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 = new ResultSetMappingBuilder($this->em);
$rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w'); $rsm->addRootEntityFromClassMetadata(AccompanyingPeriodWork::class, 'w');
$sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w $sql = "SELECT {$rsm} FROM chill_person_accompanying_period_work w
WHERE accompanyingPeriod_id = :periodId LEFT JOIN chill_person_accompanying_period_work_referrer AS rw ON accompanyingperiodwork_id = w.id
ORDER BY WHERE accompanyingPeriod_id = :periodId";
CASE WHEN enddate IS NULL THEN '-infinity'::timestamp ELSE 'infinity'::timestamp END ASC,
startdate DESC, // implement filters
enddate DESC,
id DESC if ([] !== ($filters['types'] ?? [])) {
LIMIT :limit OFFSET :offset"; $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) $nq = $this->em->createNativeQuery($sql, $rsm)
->setParameter('periodId', $period->getId(), Types::INTEGER) ->setParameter('periodId', $period->getId(), Types::INTEGER)
->setParameter('types', $typeIds)
->setParameter('after', $after)
->setParameter('before', $before)
->setParameter('limit', $limit, Types::INTEGER) ->setParameter('limit', $limit, Types::INTEGER)
->setParameter('offset', $offset, Types::INTEGER); ->setParameter('offset', $offset, Types::INTEGER);
foreach ($filters['user'] as $key => $user) {
$nq->setParameter('user_' . $key, $user);
}
return $nq->getResult(); return $nq->getResult();
} }
/**
* Return a list of types of social actions associated to the accompanying period
*
* @return array<SocialAction>
*/
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 public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
{ {
return $this->buildQueryNearEndDateByUser($user, $since, $until) return $this->buildQueryNearEndDateByUser($user, $since, $until)

View File

@ -5,18 +5,23 @@
{% block js %} {% block js %}
{{ parent() }} {{ parent() }}
{{ encore_entry_script_tags('mod_entity_workflow_pick') }} {{ encore_entry_script_tags('mod_entity_workflow_pick') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %} {% endblock %}
{% block css %} {% block css %}
{{ parent() }} {{ parent() }}
{{ encore_entry_link_tags('mod_entity_workflow_pick') }} {{ encore_entry_link_tags('mod_entity_workflow_pick') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="accompanying-course-work"> <div class="accompanying-course-work">
<h1>{{ block('title') }}</h1> <h1>{{ block('title') }}</h1>
{{ filter|chill_render_filter_order_helper }}
{% if works|length == 0 %} {% if works|length == 0 %}
<p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p> <p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
{% else %} {% else %}

View File

@ -913,6 +913,9 @@ accompanying_course_work:
social_evaluation: Évaluation social_evaluation: Évaluation
private_comment: Commentaire privé private_comment: Commentaire privé
timeSpent: Temps de rédaction timeSpent: Temps de rédaction
date_filter: Filtrer par date
types_filter: Filtrer par type d'action
user_filter: Filtrer par intervenant
# #