mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
add regulation list and basic regulation list query
This commit is contained in:
parent
8982697a73
commit
da9eba2618
@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\PersonBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Location;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\UserJob;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
|
||||
class AccompanyingPeriodRegulationListController
|
||||
{
|
||||
private AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
|
||||
|
||||
private EngineInterface $engine;
|
||||
|
||||
private FormFactoryInterface $formFactory;
|
||||
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private Security $security;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, EngineInterface $engine, FormFactoryInterface $formFactory, PaginatorFactory $paginatorFactory, Security $security, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) %}
|
||||
<div class="item-col item-meta">
|
||||
{% set job_id = null %}
|
||||
{% if period.job is defined %}
|
||||
{% set job_id = period.job.id %}
|
||||
{% endif %}
|
||||
<span
|
||||
data-set-referrer-app="data-set-referrer-app"
|
||||
data-set-referrer-accompanying-period-id="{{ period.id }}"
|
||||
data-set-referrer-job-id="{{ job_id }}"
|
||||
></span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro period_actions(period) %}
|
||||
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': period.id}) }}" class="btn btn-show"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% import _self as m %}
|
||||
|
||||
{% block content %}
|
||||
<div class="col-10">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
{{ form_start(form) }}
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
{{ form_label(form.locations ) }}
|
||||
{{ form_widget(form.locations, {'attr': {'class': 'select2'}}) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_label(form.jobs) }}
|
||||
{{ form_widget(form.jobs, {'attr': {'class': 'select2'}}) }}
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
{{ form_label(form.services) }}
|
||||
{{ form_widget(form.services, {'attr': {'class': 'select2'}}) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save change-icon">
|
||||
<i class="fa fa-filter"></i> Filtrer
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
|
||||
{% if periods|length == 0 %}
|
||||
<p class="chill-no-data-statement">Aucun parcours à désigner, ou droits insuffisants pour les afficher</p>
|
||||
{% else %}
|
||||
|
||||
<p><span class="badge rounded-pill bg-primary">{{ paginator.totalItems }}</span> parcours à attribuer (calculé ce jour à {{ null|format_time('medium') }})</p>
|
||||
|
||||
<div class="flex-table">
|
||||
{% for period in periods %}
|
||||
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
|
||||
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{{ chill_pagination(paginator) }}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Controller;
|
||||
|
||||
use Chill\MainBundle\Test\PrepareClientTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class AccompanyingPeriodRegulationListControllerTest extends WebTestCase
|
||||
{
|
||||
use PrepareClientTrait;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
self::bootKernel();
|
||||
}
|
||||
|
||||
public function testRegulationList(): void
|
||||
{
|
||||
$client = $this->getClientAuthenticated();
|
||||
|
||||
$client->request('GET', '/fr/person/periods/undispatched');
|
||||
|
||||
$this->assertResponseIsSuccessful();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user