mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 11:18:25 +00:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			2.19.0
			...
			deploy/qui
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 014170989e | |||
| c26d342d30 | |||
| 2cf28b6c2d | |||
| ed5351da14 | |||
| 8dca8d9b37 | |||
| 1cddad5c66 | |||
| c98eda93cf | |||
| dd991e3572 | |||
| 0640631821 | |||
| e845d9ba90 | |||
| 
						 | 
					6f8231f6f6 | ||
| 
						 | 
					048161e300 | ||
| 
						 | 
					4f49292178 | ||
| 357bf192b8 | |||
| 6eaffcae49 | |||
| 
						 | 
					f92d710a26 | ||
| 
						 | 
					bc6ba88acd | ||
| efefb64119 | |||
| 6fa882f60f | |||
| 484058dcfc | |||
| 
						 | 
					b72d45d9db | ||
| 2a1f5cbad1 | |||
| e8566fd006 | |||
| cd6b5c9a39 | |||
| 9aa3974071 | |||
| a8bf478ee8 | |||
| a35f3363b2 | |||
| b7513068da | |||
| 2dd71ea197 | |||
| 757c064b7b | |||
| f5d38f606c | |||
| 5abe482b12 | |||
| 5ded4822a2 | |||
| 6122a5d62f | 
@@ -4,6 +4,7 @@ namespace Chill\ActivityBundle\Menu;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
 | 
			
		||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Knp\Menu\MenuItem;
 | 
			
		||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
@@ -34,21 +35,13 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
 | 
			
		||||
    {
 | 
			
		||||
        $period = $parameters['accompanyingCourse'];
 | 
			
		||||
 | 
			
		||||
        $menu->addChild($this->translator->trans('Activity list'), [
 | 
			
		||||
            'route' => 'chill_activity_activity_list',
 | 
			
		||||
            'routeParameters' => [
 | 
			
		||||
                'accompanying_period_id' => $period->getId(),
 | 
			
		||||
            ]])
 | 
			
		||||
            ->setExtras(['order' => 40]);
 | 
			
		||||
 | 
			
		||||
        $menu->addChild($this->translator->trans('Add a new activity'), [
 | 
			
		||||
            'route' => 'chill_activity_activity_select_type',
 | 
			
		||||
            'routeParameters' => [
 | 
			
		||||
                'accompanying_period_id' => $period->getId(),
 | 
			
		||||
            ]])
 | 
			
		||||
            ->setExtras(['order' => 41]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
 | 
			
		||||
            $menu->addChild($this->translator->trans('Activity list'), [
 | 
			
		||||
                'route' => 'chill_activity_activity_list',
 | 
			
		||||
                'routeParameters' => [
 | 
			
		||||
                    'accompanying_period_id' => $period->getId(),
 | 
			
		||||
                ]])
 | 
			
		||||
                ->setExtras(['order' => 40]);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -55,24 +55,26 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
 | 
			
		||||
    {
 | 
			
		||||
        /* @var $person \Chill\PersonBundle\Entity\Person */
 | 
			
		||||
        $person = $parameters['person'];
 | 
			
		||||
        
 | 
			
		||||
        if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
 | 
			
		||||
            $menu->addChild(
 | 
			
		||||
                $this->translator->trans('Activity list'), [
 | 
			
		||||
                    'route' => 'chill_activity_activity_list',
 | 
			
		||||
                    'routeParameters' => [ 'person_id' => $person->getId() ],
 | 
			
		||||
                ])
 | 
			
		||||
                ->setExtra('order', 201)
 | 
			
		||||
                ;
 | 
			
		||||
        }
 | 
			
		||||
        if ($this->authorizationChecker->isGranted(ActivityVoter::CREATE, $person)) {
 | 
			
		||||
            $menu->addChild(
 | 
			
		||||
                $this->translator->trans('Add a new activity'), [
 | 
			
		||||
                    'route' => 'chill_activity_activity_new',
 | 
			
		||||
                    'routeParameters' => [ 'person_id' => $person->getId() ],
 | 
			
		||||
                ])
 | 
			
		||||
                ->setExtra('order', 200)
 | 
			
		||||
                ;
 | 
			
		||||
 | 
			
		||||
        if (false) {
 | 
			
		||||
            if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
 | 
			
		||||
                $menu->addChild(
 | 
			
		||||
                    $this->translator->trans('Activity list'), [
 | 
			
		||||
                        'route' => 'chill_activity_activity_list',
 | 
			
		||||
                        'routeParameters' => [ 'person_id' => $person->getId() ],
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->setExtra('order', 201)
 | 
			
		||||
                    ;
 | 
			
		||||
            }
 | 
			
		||||
            if ($this->authorizationChecker->isGranted(ActivityVoter::CREATE, $person)) {
 | 
			
		||||
                $menu->addChild(
 | 
			
		||||
                    $this->translator->trans('Add a new activity'), [
 | 
			
		||||
                        'route' => 'chill_activity_activity_new',
 | 
			
		||||
                        'routeParameters' => [ 'person_id' => $person->getId() ],
 | 
			
		||||
                    ])
 | 
			
		||||
                    ->setExtra('order', 200)
 | 
			
		||||
                    ;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ use Chill\MainBundle\Search\SearchProvider;
 | 
			
		||||
use Symfony\Contracts\Translation\TranslatorInterface;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApi;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Class SearchController
 | 
			
		||||
@@ -151,11 +152,14 @@ class SearchController extends AbstractController
 | 
			
		||||
    {
 | 
			
		||||
        //TODO this is an incomplete implementation
 | 
			
		||||
        $query = $request->query->get('q', '');
 | 
			
		||||
        $types = $request->query->get('type', []);
 | 
			
		||||
 | 
			
		||||
        $results = $this->searchApi->getResults($query, 0, 150);
 | 
			
		||||
        $paginator = $this->paginatorFactory->create(count($results));
 | 
			
		||||
        if (count($types) === 0) {
 | 
			
		||||
            throw new BadRequestException("The request must contains at "
 | 
			
		||||
                ." one type");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $collection = new Collection($results, $paginator);
 | 
			
		||||
        $collection = $this->searchApi->getResults($query, $types, []);
 | 
			
		||||
 | 
			
		||||
        return $this->json($collection);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -132,6 +132,7 @@ class Address
 | 
			
		||||
    /**
 | 
			
		||||
     * True if the address is a "no address", aka homeless person, ...
 | 
			
		||||
     * @groups({"write"})
 | 
			
		||||
     * @ORM\Column(type="boolean")
 | 
			
		||||
     *
 | 
			
		||||
     * @var bool
 | 
			
		||||
     */
 | 
			
		||||
@@ -298,7 +299,7 @@ class Address
 | 
			
		||||
     * @param bool $isNoAddress
 | 
			
		||||
     * @return $this
 | 
			
		||||
     */
 | 
			
		||||
    public function setIsNoAddress(bool $isNoAddress)
 | 
			
		||||
    public function setIsNoAddress(bool $isNoAddress): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->isNoAddress = $isNoAddress;
 | 
			
		||||
        return $this;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,88 +2,169 @@
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Search;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
 | 
			
		||||
use Chill\MainBundle\Serializer\Model\Collection;
 | 
			
		||||
use Chill\MainBundle\Pagination\PaginatorFactory;
 | 
			
		||||
use Chill\PersonBundle\Search\SearchPersonApiProvider;
 | 
			
		||||
use Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch;
 | 
			
		||||
use Doctrine\DBAL\Types\Types;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
 | 
			
		||||
use Chill\MainBundle\Search\SearchProvider;
 | 
			
		||||
use Symfony\Component\VarDumper\Resources\functions\dump;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *  ***Warning*** This is an incomplete implementation ***Warning***
 | 
			
		||||
 */
 | 
			
		||||
class SearchApi
 | 
			
		||||
{
 | 
			
		||||
    private EntityManagerInterface $em;
 | 
			
		||||
    private SearchProvider $search;
 | 
			
		||||
    private PaginatorFactory $paginator;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $em, SearchProvider $search)
 | 
			
		||||
    private array $providers = [];
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        EntityManagerInterface $em,
 | 
			
		||||
        SearchPersonApiProvider $searchPerson,
 | 
			
		||||
        ThirdPartyApiSearch $thirdPartyApiSearch,
 | 
			
		||||
        PaginatorFactory $paginator
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        $this->em = $em;
 | 
			
		||||
        $this->search = $search;
 | 
			
		||||
        $this->providers[] = $searchPerson;
 | 
			
		||||
        $this->providers[] = $thirdPartyApiSearch;
 | 
			
		||||
        $this->paginator = $paginator;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return Model/Result[]
 | 
			
		||||
     */
 | 
			
		||||
    public function getResults(string $query, int $offset, int $maxResult): array
 | 
			
		||||
    public function getResults(string $pattern, array $types, array $parameters): Collection
 | 
			
		||||
    {
 | 
			
		||||
        // **warning again**: this is an incomplete implementation 
 | 
			
		||||
        $results = [];
 | 
			
		||||
        $queries = $this->findQueries($pattern, $types, $parameters);
 | 
			
		||||
 | 
			
		||||
        foreach ($this->getPersons($query) as $p) {
 | 
			
		||||
            $results[] = new Model\Result((float)\rand(0, 100) / 100, $p);
 | 
			
		||||
        }
 | 
			
		||||
        foreach ($this->getThirdParties($query) as $t) {
 | 
			
		||||
            $results[] = new Model\Result((float)\rand(0, 100) / 100, $t);
 | 
			
		||||
        }
 | 
			
		||||
        $total = $this->countItems($queries, $types, $parameters);
 | 
			
		||||
        $paginator = $this->paginator->create($total);
 | 
			
		||||
 | 
			
		||||
        \usort($results, function(Model\Result $a, Model\Result $b) {
 | 
			
		||||
            return ($a->getRelevance() <=> $b->getRelevance()) * -1;
 | 
			
		||||
        });
 | 
			
		||||
        $rawResults = $this->fetchRawResult($queries, $types, $parameters, $paginator);
 | 
			
		||||
 | 
			
		||||
        return $results;
 | 
			
		||||
        $this->prepareProviders($rawResults);
 | 
			
		||||
        $results = $this->buildResults($rawResults);
 | 
			
		||||
 | 
			
		||||
        $collection = new Collection($results, $paginator);
 | 
			
		||||
 | 
			
		||||
        return $collection;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function countResults(string $query): int
 | 
			
		||||
    private function findQueries($pattern, array $types, array $parameters): array
 | 
			
		||||
    {
 | 
			
		||||
        return 0;
 | 
			
		||||
        return \array_map(
 | 
			
		||||
            fn($p) => $p->provideQuery($pattern, $parameters),
 | 
			
		||||
            $this->findProviders($pattern, $types, $parameters),
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getThirdParties(string $query)
 | 
			
		||||
    private function findProviders(string $pattern, array $types, array $parameters): array
 | 
			
		||||
    {
 | 
			
		||||
        $thirdPartiesIds = $this->em->createQuery('SELECT t.id FROM '.ThirdParty::class.' t')
 | 
			
		||||
            ->getScalarResult();
 | 
			
		||||
        $nbResults = rand(0, 15);
 | 
			
		||||
 | 
			
		||||
        if ($nbResults === 1) {
 | 
			
		||||
            $nbResults++;
 | 
			
		||||
        } elseif ($nbResults === 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
        $ids = \array_map(function ($e) use ($thirdPartiesIds) { return $thirdPartiesIds[$e]['id'];}, 
 | 
			
		||||
            \array_rand($thirdPartiesIds, $nbResults));
 | 
			
		||||
 | 
			
		||||
        $a = $this->em->getRepository(ThirdParty::class)
 | 
			
		||||
            ->findById($ids);
 | 
			
		||||
        return $a;
 | 
			
		||||
        return \array_filter(
 | 
			
		||||
            $this->providers,
 | 
			
		||||
            fn($p) => $p->supportsTypes($pattern, $types, $parameters)
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function getPersons(string $query)
 | 
			
		||||
    private function countItems($providers, $types, $parameters): int
 | 
			
		||||
    {
 | 
			
		||||
        $params = [
 | 
			
		||||
            SearchInterface::SEARCH_PREVIEW_OPTION => false
 | 
			
		||||
            ];
 | 
			
		||||
        $search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json');
 | 
			
		||||
        $ids = \array_map(function($r) { return $r['id']; }, $search['results']);
 | 
			
		||||
        list($countQuery, $parameters) = $this->buildCountQuery($providers, $types, $parameters);
 | 
			
		||||
        $rsmCount = new ResultSetMappingBuilder($this->em);
 | 
			
		||||
        $rsmCount->addScalarResult('count', 'count');
 | 
			
		||||
        $countNq = $this->em->createNativeQuery($countQuery, $rsmCount);
 | 
			
		||||
        $countNq->setParameters($parameters);
 | 
			
		||||
        
 | 
			
		||||
        return $countNq->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildCountQuery(array $queries, $types, $parameters)
 | 
			
		||||
    {
 | 
			
		||||
        $query = "SELECT COUNT(sq.key) AS count FROM ({union_unordered}) AS sq";
 | 
			
		||||
        $unions = [];
 | 
			
		||||
        $parameters = [];
 | 
			
		||||
 | 
			
		||||
        if (count($ids) === 0) {
 | 
			
		||||
            return [];
 | 
			
		||||
        foreach ($queries as $q) {
 | 
			
		||||
            $unions[] = $q->buildQuery();
 | 
			
		||||
            $parameters = \array_merge($parameters, $q->buildParameters());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->em->getRepository(Person::class)
 | 
			
		||||
            ->findById($ids)
 | 
			
		||||
        $unionUnordered = \implode(" UNION ", $unions);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            \strtr($query, [ '{union_unordered}' => $unionUnordered ]),
 | 
			
		||||
            $parameters
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildUnionQuery(array $queries, $types, $parameters)
 | 
			
		||||
    {
 | 
			
		||||
        $query = "{unions} ORDER BY pertinence DESC";
 | 
			
		||||
        $unions = [];
 | 
			
		||||
        $parameters = [];
 | 
			
		||||
 | 
			
		||||
        foreach ($queries as $q) {
 | 
			
		||||
            $unions[] = $q->buildQuery();
 | 
			
		||||
            $parameters = \array_merge($parameters, $q->buildParameters());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $union = \implode(" UNION ", $unions);
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            \strtr($query, [ '{unions}' => $union]),
 | 
			
		||||
            $parameters
 | 
			
		||||
        ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function fetchRawResult($queries, $types, $parameters, $paginator): array
 | 
			
		||||
    {
 | 
			
		||||
        list($union, $parameters) = $this->buildUnionQuery($queries, $types, $parameters, $paginator);
 | 
			
		||||
        $rsm = new ResultSetMappingBuilder($this->em);
 | 
			
		||||
        $rsm->addScalarResult('key', 'key', Types::STRING)
 | 
			
		||||
            ->addScalarResult('metadata', 'metadata', Types::JSON)
 | 
			
		||||
            ->addScalarResult('pertinence', 'pertinence', Types::FLOAT)
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
        $nq = $this->em->createNativeQuery($union, $rsm);
 | 
			
		||||
        $nq->setParameters($parameters);
 | 
			
		||||
        
 | 
			
		||||
        return $nq->getResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function prepareProviders($rawResults)
 | 
			
		||||
    {
 | 
			
		||||
        $metadatas = [];
 | 
			
		||||
        foreach ($rawResults as $r) {
 | 
			
		||||
            foreach ($this->providers as $k => $p) {
 | 
			
		||||
                if ($p->supportsResult($r['key'], $r['metadata'])) {
 | 
			
		||||
                    $metadatas[$k][] = $r['metadata'];
 | 
			
		||||
                    break;
 | 
			
		||||
                } 
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        foreach ($metadatas as $k => $m) {
 | 
			
		||||
            $this->providers[$k]->prepare($m);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildResults($rawResults)
 | 
			
		||||
    {
 | 
			
		||||
        foreach ($rawResults as $r) {
 | 
			
		||||
            foreach ($this->providers as $k => $p) {
 | 
			
		||||
                if ($p->supportsResult($r['key'], $r['metadata'])) {
 | 
			
		||||
                    $items[] = (new SearchApiResult($r['pertinence']))
 | 
			
		||||
                        ->setResult(
 | 
			
		||||
                            $p->getResult($r['key'], $r['metadata'], $r['pertinence'])
 | 
			
		||||
                        );
 | 
			
		||||
                    break;
 | 
			
		||||
                } 
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $items ?? [];
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiInterface.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiInterface.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Search;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiQuery;
 | 
			
		||||
 | 
			
		||||
interface SearchApiInterface
 | 
			
		||||
{
 | 
			
		||||
    public function provideQuery(string $pattern, array $parameters): SearchApiQuery;
 | 
			
		||||
 | 
			
		||||
    public function supportsTypes(string $pattern, array $types, array $parameters): bool;
 | 
			
		||||
 | 
			
		||||
    public function prepare(array $metadatas): void;
 | 
			
		||||
 | 
			
		||||
    public function supportsResult(string $key, array $metadatas): bool;
 | 
			
		||||
 | 
			
		||||
    public function getResult(string $key, array $metadata, float $pertinence);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										85
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiQuery.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiQuery.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Search;
 | 
			
		||||
 | 
			
		||||
class SearchApiQuery
 | 
			
		||||
{
 | 
			
		||||
    private ?string $selectKey;
 | 
			
		||||
    private array $selectKeyParams = [];
 | 
			
		||||
    private ?string $jsonbMetadata;
 | 
			
		||||
    private array $jsonbMetadataParams = [];
 | 
			
		||||
    private ?string $pertinence;
 | 
			
		||||
    private array $pertinenceParams = [];
 | 
			
		||||
    private ?string $fromClause;
 | 
			
		||||
    private array $fromClauseParams = [];
 | 
			
		||||
    private ?string $whereClause;
 | 
			
		||||
    private array $whereClauseParams = [];
 | 
			
		||||
 | 
			
		||||
    public function setSelectKey(string $selectKey, array $params = []): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->selectKey = $selectKey;
 | 
			
		||||
        $this->selectKeyParams = $params;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setSelectJsonbMetadata(string $jsonbMetadata, array $params = []): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->jsonbMetadata = $jsonbMetadata;
 | 
			
		||||
        $this->jsonbMetadataParams = $params;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setSelectPertinence(string $pertinence, array $params = []): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->pertinence = $pertinence;
 | 
			
		||||
        $this->pertinenceParams = $params;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setFromClause(string $fromClause, array $params = []): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->fromClause = $fromClause;
 | 
			
		||||
        $this->fromClauseParams = $params;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setWhereClause(string $whereClause, array $params = []): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->whereClause = $whereClause;
 | 
			
		||||
        $this->whereClauseParams = $params;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildQuery(): string
 | 
			
		||||
    {
 | 
			
		||||
        return \strtr("SELECT
 | 
			
		||||
            '{key}' AS key,
 | 
			
		||||
            {metadata} AS metadata,
 | 
			
		||||
            {pertinence} AS pertinence
 | 
			
		||||
        FROM {from}
 | 
			
		||||
        WHERE {where}
 | 
			
		||||
        ", [
 | 
			
		||||
            '{key}' => $this->selectKey,
 | 
			
		||||
            '{metadata}' => $this->jsonbMetadata,
 | 
			
		||||
            '{pertinence}' => $this->pertinence,
 | 
			
		||||
            '{from}' => $this->fromClause,
 | 
			
		||||
            '{where}' => $this->whereClause
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function buildParameters(): array
 | 
			
		||||
    {
 | 
			
		||||
        return \array_merge(
 | 
			
		||||
            $this->selectKeyParams,
 | 
			
		||||
            $this->jsonbMetadataParams,
 | 
			
		||||
            $this->pertinenceParams,
 | 
			
		||||
            $this->fromClauseParams,
 | 
			
		||||
            $this->whereClauseParams
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										40
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiResult.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								src/Bundle/ChillMainBundle/Search/SearchApiResult.php
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\MainBundle\Search;
 | 
			
		||||
 | 
			
		||||
use Symfony\Component\Serializer\Annotation as Serializer;
 | 
			
		||||
 | 
			
		||||
class SearchApiResult
 | 
			
		||||
{
 | 
			
		||||
    private float $pertinence;
 | 
			
		||||
 | 
			
		||||
    private $result;
 | 
			
		||||
 | 
			
		||||
    public function __construct(float $relevance)
 | 
			
		||||
    {
 | 
			
		||||
        $this->relevance = $relevance;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function setResult($result): self
 | 
			
		||||
    {
 | 
			
		||||
        $this->result = $result;
 | 
			
		||||
 | 
			
		||||
        return $this;
 | 
			
		||||
    } 
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Serializer\Groups({"read"})
 | 
			
		||||
     */
 | 
			
		||||
    public function getResult()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @Serializer\Groups({"read"})
 | 
			
		||||
     */
 | 
			
		||||
    public function getRelevance(): float
 | 
			
		||||
    {
 | 
			
		||||
        return $this->relevance;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,7 +14,7 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
 | 
			
		||||
    public function normalize($address, string $format = null, array $context = [])
 | 
			
		||||
    {
 | 
			
		||||
        $data['address_id'] = $address->getId();
 | 
			
		||||
        $data['text'] = $address->getStreet().', '.$address->getStreetNumber();
 | 
			
		||||
        $data['text'] = $address->isNoAddress() ? '' : $address->getStreet().', '.$address->getStreetNumber();
 | 
			
		||||
        $data['street'] = $address->getStreet();
 | 
			
		||||
        $data['streetNumber'] = $address->getStreetNumber();
 | 
			
		||||
        $data['postcode']['name'] = $address->getPostCode()->getName();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,36 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Bundle\ChillMainBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
 | 
			
		||||
class SearchApiControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateSearchData
 | 
			
		||||
     */
 | 
			
		||||
    public function testSearch(string $pattern, array $types)
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
 | 
			
		||||
        $client->request(
 | 
			
		||||
            Request::METHOD_GET,
 | 
			
		||||
            '/api/1.0/search.json',
 | 
			
		||||
            [ 'q' => $pattern, 'type' => $types ]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertResponseIsSuccessful();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function generateSearchData()
 | 
			
		||||
    {
 | 
			
		||||
        yield ['per', ['person', 'thirdparty'] ];
 | 
			
		||||
        yield ['per', ['thirdparty'] ];
 | 
			
		||||
        yield ['per', ['person'] ];
 | 
			
		||||
        yield ['fjklmeqjfkdqjklrmefdqjklm', ['person', 'thirdparty'] ];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,6 +5,5 @@ services:
 | 
			
		||||
    Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
 | 
			
		||||
 | 
			
		||||
    Chill\MainBundle\Search\SearchApi:
 | 
			
		||||
        arguments:
 | 
			
		||||
          $em: '@Doctrine\ORM\EntityManagerInterface'
 | 
			
		||||
          $search: '@Chill\MainBundle\Search\SearchProvider'
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
 | 
			
		||||
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
 | 
			
		||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
 | 
			
		||||
@@ -35,14 +36,18 @@ class AccompanyingCourseController extends Controller
 | 
			
		||||
 | 
			
		||||
    protected ValidatorInterface $validator;
 | 
			
		||||
 | 
			
		||||
    private AccompanyingPeriodWorkRepository $workRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        SerializerInterface $serializer,
 | 
			
		||||
        EventDispatcherInterface $dispatcher,
 | 
			
		||||
        ValidatorInterface $validator
 | 
			
		||||
        ValidatorInterface $validator,
 | 
			
		||||
        AccompanyingPeriodWorkRepository $workRepository
 | 
			
		||||
    ) {
 | 
			
		||||
        $this->serializer = $serializer;
 | 
			
		||||
        $this->dispatcher = $dispatcher;
 | 
			
		||||
        $this->validator = $validator;
 | 
			
		||||
        $this->workRepository = $workRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -102,9 +107,16 @@ class AccompanyingCourseController extends Controller
 | 
			
		||||
            ['date' => 'DESC'],
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $works = $this->workRepository->findByAccompanyingPeriod(
 | 
			
		||||
            $accompanyingCourse,
 | 
			
		||||
            ['startDate' => 'DESC', 'endDate' => 'DESC'],
 | 
			
		||||
            3
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
 | 
			
		||||
            'accompanyingCourse' => $accompanyingCourse,
 | 
			
		||||
            'withoutHousehold' => $withoutHousehold,
 | 
			
		||||
            'works' => $works,
 | 
			
		||||
            'activities' => $activities
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -47,12 +47,12 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
     * @var EventDispatcherInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $eventDispatcher;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @var ValidatorInterface
 | 
			
		||||
     */
 | 
			
		||||
    protected $validator;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * AccompanyingPeriodController constructor.
 | 
			
		||||
     *
 | 
			
		||||
@@ -64,23 +64,25 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
        $this->eventDispatcher = $eventDispatcher;
 | 
			
		||||
        $this->validator = $validator;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public function listAction(int $person_id): Response
 | 
			
		||||
    {
 | 
			
		||||
        $person = $this->_getPerson($person_id);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
        $event = new PrivacyEvent($person, [
 | 
			
		||||
            'element_class' => AccompanyingPeriod::class,
 | 
			
		||||
            'action' => 'list'
 | 
			
		||||
        ]);
 | 
			
		||||
        $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
 | 
			
		||||
 | 
			
		||||
        $accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
 | 
			
		||||
 | 
			
		||||
        return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
 | 
			
		||||
            'accompanying_periods' => $person->getAccompanyingPeriodsOrdered(),
 | 
			
		||||
            'accompanying_periods' => $accompanyingPeriods,
 | 
			
		||||
            'person' => $person
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public function createAction(int $person_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $person = $this->_getPerson($person_id);
 | 
			
		||||
@@ -90,17 +92,17 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        $accompanyingPeriod = new AccompanyingPeriod(new \DateTime('now'));
 | 
			
		||||
        $accompanyingPeriod->setClosingDate(new \DateTime('now'));
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $accompanyingPeriod->addPerson($person);
 | 
			
		||||
        //or $person->addAccompanyingPeriod($accompanyingPeriod);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $form = $this->createForm(
 | 
			
		||||
            AccompanyingPeriodType::class,
 | 
			
		||||
            $accompanyingPeriod, [
 | 
			
		||||
                'period_action' => 'create',
 | 
			
		||||
                'center' => $person->getCenter()
 | 
			
		||||
            ]);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if ($request->getMethod() === 'POST') {
 | 
			
		||||
            $form->handleRequest($request);
 | 
			
		||||
            $errors = $this->_validatePerson($person);
 | 
			
		||||
@@ -120,7 +122,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
                    $this->generateUrl('chill_person_accompanying_period_list', [
 | 
			
		||||
                        'person_id' => $person->getId()
 | 
			
		||||
                    ]));
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                $flashBag->add('error', $this->get('translator')
 | 
			
		||||
                        ->trans('Error! Period not created!'));
 | 
			
		||||
@@ -137,7 +139,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
            'accompanying_period' => $accompanyingPeriod
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
@@ -154,7 +156,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        /** @var Person $person */
 | 
			
		||||
        $person = $this->_getPerson($person_id);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        // CHECK
 | 
			
		||||
        if (! $accompanyingPeriod->containsPerson($person)) {
 | 
			
		||||
            throw new Exception("Accompanying period " . $period_id . " does not contain person " . $person_id);
 | 
			
		||||
@@ -176,7 +178,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
            if ($form->isValid(['Default', 'closed'])
 | 
			
		||||
                    && count($errors) === 0) {
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                $em->flush();
 | 
			
		||||
 | 
			
		||||
                $flashBag->add('success',
 | 
			
		||||
@@ -186,9 +188,9 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
                    $this->generateUrl('chill_person_accompanying_period_list', [
 | 
			
		||||
                        'person_id' => $person->getId()
 | 
			
		||||
                ]));
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
                $flashBag->add('error', $this->get('translator')
 | 
			
		||||
                        ->trans('Error when updating the period'));
 | 
			
		||||
 | 
			
		||||
@@ -204,19 +206,19 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
            'accompanying_period' => $accompanyingPeriod
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @throws \Exception
 | 
			
		||||
     */
 | 
			
		||||
    public function closeAction(int $person_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $person = $this->_getPerson($person_id);
 | 
			
		||||
 | 
			
		||||
        $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, 'You are not allowed to update this person');
 | 
			
		||||
 | 
			
		||||
        if ($person->isOpen() === false) {
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            $this->get('session')->getFlashBag()
 | 
			
		||||
                ->add('error', $this->get('translator')
 | 
			
		||||
                    ->trans('Beware period is closed', ['%name%' => $person->__toString()]
 | 
			
		||||
@@ -229,7 +231,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $current = $person->getCurrentAccompanyingPeriod();
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        $form = $this->createForm(AccompanyingPeriodType::class, $current, [
 | 
			
		||||
            'period_action' => 'close',
 | 
			
		||||
            'center' => $person->getCenter()
 | 
			
		||||
@@ -256,7 +258,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
                            'person_id' => $person->getId()
 | 
			
		||||
                        ])
 | 
			
		||||
                    );
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
                } else {
 | 
			
		||||
                    $this->get('session')->getFlashBag()
 | 
			
		||||
                            ->add('error', $this->get('translator')
 | 
			
		||||
@@ -267,7 +269,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
                            ->add('info', $error->getMessage());
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
            } else { //if form is not valid
 | 
			
		||||
                $this->get('session')->getFlashBag()
 | 
			
		||||
                    ->add('error',
 | 
			
		||||
@@ -288,7 +290,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
            'accompanying_period' => $current
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    private function _validatePerson(Person $person): ConstraintViolationListInterface
 | 
			
		||||
    {
 | 
			
		||||
        $errors = $this->validator->validate($person, null,
 | 
			
		||||
@@ -296,10 +298,10 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        // Can be disabled with config
 | 
			
		||||
        if (false === $this->container->getParameter('chill_person.allow_multiple_simultaneous_accompanying_periods')) {
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            $errors_accompanying_period = $this->validator->validate($person, null,
 | 
			
		||||
                ['accompanying_period_consistent']);
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
            foreach($errors_accompanying_period as $error ) {
 | 
			
		||||
                $errors->add($error);
 | 
			
		||||
            }
 | 
			
		||||
@@ -307,7 +309,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        return $errors;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public function openAction(int $person_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        $person = $this->_getPerson($person_id);
 | 
			
		||||
@@ -384,7 +386,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
            'accompanying_period' => $accompanyingPeriod
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public function reOpenAction(int $person_id, int $period_id, Request $request): Response
 | 
			
		||||
    {
 | 
			
		||||
        /** @var Person $person */
 | 
			
		||||
@@ -392,7 +394,7 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
 | 
			
		||||
        /* @var $period AccompanyingPeriod */
 | 
			
		||||
        $period = \array_filter(
 | 
			
		||||
            $person->getAccompanyingPeriods(), 
 | 
			
		||||
            $person->getAccompanyingPeriods(),
 | 
			
		||||
            function (AccompanyingPeriod $p) use ($period_id) {
 | 
			
		||||
                return $p->getId() === ($period_id);
 | 
			
		||||
            }
 | 
			
		||||
@@ -417,13 +419,13 @@ class AccompanyingPeriodController extends AbstractController
 | 
			
		||||
            return $this->redirectToRoute('chill_person_accompanying_period_list', [
 | 
			
		||||
                'person_id' => $person->getId()
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        } elseif ($confirm === false && $period->canBeReOpened($person)) {
 | 
			
		||||
            return $this->render('ChillPersonBundle:AccompanyingPeriod:re_open.html.twig', [
 | 
			
		||||
                'period' => $period,
 | 
			
		||||
                'person' => $person
 | 
			
		||||
            ]);
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        } else {
 | 
			
		||||
            return (new Response())
 | 
			
		||||
                ->setStatusCode(Response::HTTP_BAD_REQUEST)
 | 
			
		||||
 
 | 
			
		||||
@@ -4,15 +4,50 @@ namespace Chill\PersonBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\CRUD\Controller\ApiController;
 | 
			
		||||
use Chill\MainBundle\Entity\Address;
 | 
			
		||||
use Chill\MainBundle\Serializer\Model\Collection;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Response;
 | 
			
		||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
 | 
			
		||||
 | 
			
		||||
class HouseholdApiController extends ApiController
 | 
			
		||||
{
 | 
			
		||||
    private HouseholdRepository $householdRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(HouseholdRepository $householdRepository)
 | 
			
		||||
    {
 | 
			
		||||
        $this->householdRepository = $householdRepository;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public function householdAddressApi($id, Request $request, string $_format): Response
 | 
			
		||||
    {
 | 
			
		||||
        return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Find Household of people participating to the same AccompanyingPeriod
 | 
			
		||||
     *
 | 
			
		||||
     * @ParamConverter("person", options={"id" = "person_id"})
 | 
			
		||||
     */
 | 
			
		||||
    public function suggestHouseholdByAccompanyingPeriodParticipationApi(Person $person, string $_format)
 | 
			
		||||
    {
 | 
			
		||||
        // TODO add acl
 | 
			
		||||
 | 
			
		||||
        $count = $this->householdRepository->countByAccompanyingPeriodParticipation($person); 
 | 
			
		||||
        $paginator = $this->getPaginatorFactory()->create($count);
 | 
			
		||||
 | 
			
		||||
        if ($count === 0) {
 | 
			
		||||
            $households = [];
 | 
			
		||||
        } else {
 | 
			
		||||
            $households = $this->householdRepository->findByAccompanyingPeriodParticipation($person,
 | 
			
		||||
                $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $collection = new Collection($households, $paginator);
 | 
			
		||||
 | 
			
		||||
        return $this->json($collection, Response::HTTP_OK, [],
 | 
			
		||||
            [ "groups" => ["read"]]);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@
 | 
			
		||||
namespace Chill\PersonBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\Household\Position;
 | 
			
		||||
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
 | 
			
		||||
use Chill\PersonBundle\Security\Authorization\PersonVoter;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Entity\Household\Household;
 | 
			
		||||
@@ -25,11 +26,17 @@ class HouseholdMemberController extends ApiController
 | 
			
		||||
 | 
			
		||||
    private TranslatorInterface $translator;
 | 
			
		||||
 | 
			
		||||
    private AccompanyingPeriodRepository $periodRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(UrlGeneratorInterface $generator, TranslatorInterface $translator)
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        UrlGeneratorInterface $generator,
 | 
			
		||||
        TranslatorInterface $translator,
 | 
			
		||||
        AccompanyingPeriodRepository $periodRepository
 | 
			
		||||
    )
 | 
			
		||||
    {
 | 
			
		||||
        $this->generator = $generator;
 | 
			
		||||
        $this->translator = $translator;
 | 
			
		||||
        $this->periodRepository = $periodRepository;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
@@ -144,8 +151,23 @@ class HouseholdMemberController extends ApiController
 | 
			
		||||
            'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'),
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        // context
 | 
			
		||||
        if ($request->query->has('accompanying_period_id')) {
 | 
			
		||||
            $period = $this->periodRepository->find(
 | 
			
		||||
                $request->query->getInt('accompanying_period_id')
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            if ($period === null) {
 | 
			
		||||
                throw $this->createNotFoundException('period not found');
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // TODO add acl on accompanying Course
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return $this->render('@ChillPerson/Household/members_editor.html.twig', [
 | 
			
		||||
            'data' => $data 
 | 
			
		||||
            'data' => $data,
 | 
			
		||||
            'expandSuggestions' => (int) $request->query->getBoolean('expand_suggestions', false),
 | 
			
		||||
            'accompanyingCourse' => $period ?? null,
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -565,6 +565,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
 | 
			
		||||
                            ],
 | 
			
		||||
                            'controller_action' => 'householdAddressApi'
 | 
			
		||||
                        ],
 | 
			
		||||
                        'suggestHouseholdByAccompanyingPeriodParticipation' => [
 | 
			
		||||
                            'path' => '/suggest/by-person/{person_id}/through-accompanying-period-participation.{_format}',
 | 
			
		||||
                            'methods' => [
 | 
			
		||||
                                Request::METHOD_GET => true,
 | 
			
		||||
                                Request::METHOD_HEAD => true,
 | 
			
		||||
                            ]
 | 
			
		||||
                        ]
 | 
			
		||||
                    ]
 | 
			
		||||
                ],
 | 
			
		||||
                [
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,9 @@ namespace Chill\PersonBundle\Repository;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
final class AccompanyingPeriodRepository
 | 
			
		||||
final class AccompanyingPeriodRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private EntityRepository $repository;
 | 
			
		||||
 | 
			
		||||
@@ -34,4 +35,32 @@ final class AccompanyingPeriodRepository
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $entityManager->getRepository(AccompanyingPeriod::class);
 | 
			
		||||
    }
 | 
			
		||||
    public function find($id): ?AccompanyingPeriod
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return AccompanyingPeriod[]
 | 
			
		||||
     */
 | 
			
		||||
    public function findAll(): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): ?AccompanyingPeriod
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria): ?AccompanyingPeriod
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    {
 | 
			
		||||
        return AccompanyingPeriod::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,102 @@
 | 
			
		||||
namespace Chill\PersonBundle\Repository\Household;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\Household\Household;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
final class HouseholdRepository
 | 
			
		||||
final class HouseholdRepository implements ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    private EntityRepository $repository;
 | 
			
		||||
    private EntityManagerInterface $em;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $entityManager)
 | 
			
		||||
    {
 | 
			
		||||
        $this->repository = $entityManager->getRepository(Household::class);
 | 
			
		||||
        $this->em = $entityManager;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function find($id)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->find($id);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAll()
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findOneBy(array $criteria)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->findOneBy($criteria);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getClassName()
 | 
			
		||||
    {
 | 
			
		||||
        return Household::class;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function countByAccompanyingPeriodParticipation(Person $person)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->buildQueryByAccompanyingPeriodParticipation($person, true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findByAccompanyingPeriodParticipation(Person $person, int $limit, int $offset)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->buildQueryByAccompanyingPeriodParticipation($person, false, $limit, $offset);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildQueryByAccompanyingPeriodParticipation(Person $person, bool $isCount = false, int $limit = 50, int $offset = 0)
 | 
			
		||||
    {
 | 
			
		||||
        $rsm = new ResultSetMappingBuilder($this->em);
 | 
			
		||||
        $rsm->addRootEntityFromClassMetadata(Household::class, 'h');
 | 
			
		||||
 | 
			
		||||
        if ($isCount) {
 | 
			
		||||
            $rsm->addScalarResult('count', 'count');
 | 
			
		||||
            $sql = \strtr(self::SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION, [
 | 
			
		||||
                '{select}' => 'COUNT(households.*) AS count',
 | 
			
		||||
                '{limits}' => ''
 | 
			
		||||
            ]);
 | 
			
		||||
        } else {
 | 
			
		||||
            $sql = \strtr(self::SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION, [
 | 
			
		||||
                '{select}' => $rsm->generateSelectClause(['h' => 'households']),
 | 
			
		||||
                '{limits}' => "OFFSET {$offset} LIMIT {$limit}"
 | 
			
		||||
            ]);
 | 
			
		||||
        }
 | 
			
		||||
        $native = $this->em->createNativeQuery($sql, $rsm);
 | 
			
		||||
        $native->setParameters([0 => $person->getId(), 1 => $person->getId()]);
 | 
			
		||||
 | 
			
		||||
        if ($isCount) {
 | 
			
		||||
            return $native->getSingleScalarResult();
 | 
			
		||||
        } else {
 | 
			
		||||
            return $native->getResult();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private CONST SQL_BY_ACCOMPANYING_PERIOD_PARTICIPATION = <<<SQL
 | 
			
		||||
WITH participations AS (
 | 
			
		||||
	SELECT DISTINCT part.accompanyingperiod_id
 | 
			
		||||
	FROM chill_person_accompanying_period_participation AS part
 | 
			
		||||
	WHERE person_id = ?),
 | 
			
		||||
other_participants AS (
 | 
			
		||||
	SELECT person_id, startDate, endDate 
 | 
			
		||||
	FROM chill_person_accompanying_period_participation
 | 
			
		||||
	JOIN participations USING (accompanyingperiod_id)
 | 
			
		||||
	WHERE person_id != ? 
 | 
			
		||||
),
 | 
			
		||||
households AS (SELECT DISTINCT household.*
 | 
			
		||||
	FROM chill_person_household_members AS hmembers
 | 
			
		||||
	JOIN other_participants AS op USING (person_id)
 | 
			
		||||
	JOIN chill_person_household AS household ON hmembers.household_id = household.id
 | 
			
		||||
  WHERE daterange(op.startDate, op.endDate) && daterange(hmembers.startDate, hmembers.endDate)
 | 
			
		||||
)
 | 
			
		||||
SELECT {select} FROM households {limits}
 | 
			
		||||
SQL;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,11 @@ final class PersonRepository
 | 
			
		||||
        return $this->repository->find($id, $lockMode, $lockVersion);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findByIds($ids): array
 | 
			
		||||
    {
 | 
			
		||||
        return $this->repository->findBy(['id' => $ids]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param $centers
 | 
			
		||||
     * @param $firstResult
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,21 @@ const householdMove = (payload) => {
 | 
			
		||||
    });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fetchHouseholdSuggestionByAccompanyingPeriod = (personId) => {
 | 
			
		||||
  const url = `/api/1.0/person/household/suggest/by-person/${personId}/through-accompanying-period-participation.json`;
 | 
			
		||||
  return window.fetch(url)
 | 
			
		||||
    .then(response => {
 | 
			
		||||
      if (response.ok) {
 | 
			
		||||
        return response.json();
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      throw Error ({m: 'Error while fetching household suggestion', status: response.status});
 | 
			
		||||
    }).then(data => Promise.resolve(data.results))
 | 
			
		||||
    .catch(e => console.err(e));
 | 
			
		||||
  ;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { 
 | 
			
		||||
  householdMove,
 | 
			
		||||
  fetchHouseholdSuggestionByAccompanyingPeriod,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,23 @@
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <ul v-if="allowChangeHousehold" class="record_actions">
 | 
			
		||||
    <li v-if="!showHouseholdSuggestion">
 | 
			
		||||
      <button
 | 
			
		||||
        class="sc-button"
 | 
			
		||||
        @click="toggleHouseholdSuggestion"
 | 
			
		||||
      >
 | 
			
		||||
        {{ $tc('household_members_editor.show_household_suggestion', 
 | 
			
		||||
          countHouseholdSuggestion) }}
 | 
			
		||||
      </button>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li v-if="showHouseholdSuggestion">
 | 
			
		||||
      <button
 | 
			
		||||
        class="sc-button"
 | 
			
		||||
        @click="toggleHouseholdSuggestion"
 | 
			
		||||
      >
 | 
			
		||||
        {{ $t('household_members_editor.hide_household_suggestion') }}
 | 
			
		||||
      </button>
 | 
			
		||||
    </li>
 | 
			
		||||
    <li v-if="allowHouseholdCreate">
 | 
			
		||||
      <button class="sc-button bt-create" @click="createHousehold">
 | 
			
		||||
        {{ $t('household_members_editor.household.create_household') }}
 | 
			
		||||
@@ -30,12 +47,56 @@
 | 
			
		||||
      </button>
 | 
			
		||||
    </li>
 | 
			
		||||
  </ul>
 | 
			
		||||
 | 
			
		||||
  <div class="householdSuggestions">
 | 
			
		||||
    <div v-if="showHouseholdSuggestion">
 | 
			
		||||
      <p>{{ $t('household_members_editor.household_for_participants_accompanying_period') }}:</p>
 | 
			
		||||
      <div class="householdSuggestionList">
 | 
			
		||||
        <div
 | 
			
		||||
          v-for="h in filterHouseholdSuggestionByAccompanyingPeriod"
 | 
			
		||||
          class="item"
 | 
			
		||||
        >
 | 
			
		||||
          <household-viewer :household="h"></household-viewer>
 | 
			
		||||
 | 
			
		||||
          <ul class="record_actions">
 | 
			
		||||
            <li>
 | 
			
		||||
              <button class="sc-button" @click="selectHousehold(h)">
 | 
			
		||||
                {{ $t('household_members_editor.select_household') }}
 | 
			
		||||
              </button>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div >
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style lang="scss">
 | 
			
		||||
 | 
			
		||||
.householdSuggestionList {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: row;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: space-between;
 | 
			
		||||
 | 
			
		||||
  & > .item {
 | 
			
		||||
    margin-bottom: 0.8rem;
 | 
			
		||||
    width: calc(50% - 1rem);
 | 
			
		||||
    border: 1px solid var(--chill-light-gray);
 | 
			
		||||
    padding: 0.5rem 0.5rem 0 0.5rem;
 | 
			
		||||
 | 
			
		||||
    ul.record_actions {
 | 
			
		||||
      margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
 | 
			
		||||
import { mapGetters } from 'vuex';
 | 
			
		||||
import { mapGetters, mapState } from 'vuex';
 | 
			
		||||
import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
@@ -47,6 +108,12 @@ export default {
 | 
			
		||||
    ...mapGetters([
 | 
			
		||||
      'hasHousehold',
 | 
			
		||||
      'isHouseholdNew',
 | 
			
		||||
      'hasHouseholdSuggestion',
 | 
			
		||||
      'countHouseholdSuggestion',
 | 
			
		||||
      'filterHouseholdSuggestionByAccompanyingPeriod',
 | 
			
		||||
    ]),
 | 
			
		||||
    ...mapState([
 | 
			
		||||
      'showHouseholdSuggestion',
 | 
			
		||||
    ]),
 | 
			
		||||
    household() {
 | 
			
		||||
      return this.$store.state.household;
 | 
			
		||||
@@ -74,6 +141,12 @@ export default {
 | 
			
		||||
    },
 | 
			
		||||
    forceLeaveWithoutHousehold() {
 | 
			
		||||
      this.$store.dispatch('forceLeaveWithoutHousehold');
 | 
			
		||||
    },
 | 
			
		||||
    toggleHouseholdSuggestion() {
 | 
			
		||||
      this.$store.commit('toggleHouseholdSuggestion');
 | 
			
		||||
    },
 | 
			
		||||
    selectHousehold(h) {
 | 
			
		||||
      this.$store.dispatch('selectHousehold', h);
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ const appMessages = {
 | 
			
		||||
   fr: {
 | 
			
		||||
     household_members_editor: {
 | 
			
		||||
      household: {
 | 
			
		||||
        no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage.",
 | 
			
		||||
        no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
 | 
			
		||||
        new_household: "Nouveau ménage",
 | 
			
		||||
        create_household: "Créer un ménage",
 | 
			
		||||
        search_household: "Chercher un ménage",
 | 
			
		||||
@@ -13,7 +13,7 @@ const appMessages = {
 | 
			
		||||
        leave_without_household: "Sans nouveau ménage"
 | 
			
		||||
      },
 | 
			
		||||
      concerned: {
 | 
			
		||||
        title: "Usagers concernés",
 | 
			
		||||
        title: "Nouveaux membres du ménage",
 | 
			
		||||
        add_persons: "Ajouter d'autres usagers",
 | 
			
		||||
        search: "Rechercher des usagers",
 | 
			
		||||
        move_to: "Déplacer vers",
 | 
			
		||||
@@ -29,6 +29,10 @@ const appMessages = {
 | 
			
		||||
      remove_position: "Retirer des {position}",
 | 
			
		||||
      remove_concerned: "Ne plus transférer",
 | 
			
		||||
      household_part: "Ménage de destination",
 | 
			
		||||
      hide_household_suggestion: "Masquer les suggestions",
 | 
			
		||||
      show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',
 | 
			
		||||
      household_for_participants_accompanying_period: "Ces ménages partagent le même parcours",
 | 
			
		||||
      select_household: "Choisir ce ménage",
 | 
			
		||||
      dates_title: "Période de validité",
 | 
			
		||||
      dates: {
 | 
			
		||||
        start_date: "Début de validité",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { createStore } from 'vuex';
 | 
			
		||||
import { householdMove, householdMoveTest } from './../api.js';
 | 
			
		||||
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod } from './../api.js';
 | 
			
		||||
import { datetimeToISO } from 'ChillMainAssets/js/date.js';
 | 
			
		||||
 | 
			
		||||
const debug = process.env.NODE_ENV !== 'production';
 | 
			
		||||
@@ -14,6 +14,8 @@ const concerned = window.household_members_editor_data.persons.map(p => {
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
console.log('expand suggestions', window.household_members_editor_expand_suggestions === 1);
 | 
			
		||||
 | 
			
		||||
const store = createStore({
 | 
			
		||||
  strict: debug,
 | 
			
		||||
  state: {
 | 
			
		||||
@@ -33,11 +35,16 @@ const store = createStore({
 | 
			
		||||
    allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
 | 
			
		||||
    allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
 | 
			
		||||
    forceLeaveWithoutHousehold: false,
 | 
			
		||||
    householdSuggestionByAccompanyingPeriod: [],
 | 
			
		||||
    showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
 | 
			
		||||
    warnings: [],
 | 
			
		||||
    errors: []
 | 
			
		||||
  },
 | 
			
		||||
  getters: {
 | 
			
		||||
    isHouseholdNew(state) {
 | 
			
		||||
      if (state.household === null) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      return !Number.isInteger(state.household.id);
 | 
			
		||||
    },
 | 
			
		||||
    hasHousehold(state) {
 | 
			
		||||
@@ -46,6 +53,21 @@ const store = createStore({
 | 
			
		||||
    hasHouseholdOrLeave(state) {
 | 
			
		||||
      return state.household !== null || state.forceLeaveWithoutHousehold;
 | 
			
		||||
    },
 | 
			
		||||
    hasHouseholdSuggestion(state, getters) {
 | 
			
		||||
      return getters.filterHouseholdSuggestionByAccompanyingPeriod.length > 0;
 | 
			
		||||
    },
 | 
			
		||||
    countHouseholdSuggestion(state, getters) {
 | 
			
		||||
      return getters.filterHouseholdSuggestionByAccompanyingPeriod.length;
 | 
			
		||||
    },
 | 
			
		||||
    filterHouseholdSuggestionByAccompanyingPeriod(state) {
 | 
			
		||||
      if (state.household === null) {
 | 
			
		||||
        return state.householdSuggestionByAccompanyingPeriod;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return state.householdSuggestionByAccompanyingPeriod
 | 
			
		||||
        .filter(h => h.id !== state.household.id)
 | 
			
		||||
        ;
 | 
			
		||||
    },
 | 
			
		||||
    hasPersonsWellPositionnated(state, getters) {
 | 
			
		||||
      return getters.needsPositionning === false
 | 
			
		||||
        || (getters.persons.length > 0 && getters.concUnpositionned.length === 0);
 | 
			
		||||
@@ -172,9 +194,25 @@ const store = createStore({
 | 
			
		||||
      state.household = null;
 | 
			
		||||
      state.forceLeaveWithoutHousehold = true;
 | 
			
		||||
    },
 | 
			
		||||
    selectHousehold(state, household) {
 | 
			
		||||
      state.household = household;
 | 
			
		||||
      state.forceLeaveWithoutHousehold = false;
 | 
			
		||||
    },
 | 
			
		||||
    setHouseholdSuggestionByAccompanyingPeriod(state, households) {
 | 
			
		||||
      let existingIds = state.householdSuggestionByAccompanyingPeriod
 | 
			
		||||
        .map(h => h.id);
 | 
			
		||||
      for (let i in households) {
 | 
			
		||||
        if (!existingIds.includes(households[i].id)) {
 | 
			
		||||
          state.householdSuggestionByAccompanyingPeriod.push(households[i]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    setStartDate(state, dateI) {
 | 
			
		||||
      state.startDate = dateI;
 | 
			
		||||
    },
 | 
			
		||||
    toggleHouseholdSuggestion(state) {
 | 
			
		||||
      state.showHouseholdSuggestion = !state.showHouseholdSuggestion;
 | 
			
		||||
    },
 | 
			
		||||
    setWarnings(state, warnings) {
 | 
			
		||||
      state.warnings = warnings;
 | 
			
		||||
      // reset errors, which should come from servers
 | 
			
		||||
@@ -213,6 +251,10 @@ const store = createStore({
 | 
			
		||||
      commit('forceLeaveWithoutHousehold');
 | 
			
		||||
      dispatch('computeWarnings');
 | 
			
		||||
    },
 | 
			
		||||
    selectHousehold({ commit }, h) {
 | 
			
		||||
      commit('selectHousehold', h);
 | 
			
		||||
      dispatch('computeWarnings');
 | 
			
		||||
    },
 | 
			
		||||
    setStartDate({ commit, dispatch }, date) {
 | 
			
		||||
      commit('setStartDate', date);
 | 
			
		||||
      dispatch('computeWarnings');
 | 
			
		||||
@@ -220,6 +262,12 @@ const store = createStore({
 | 
			
		||||
    setComment({ commit }, payload) {
 | 
			
		||||
      commit('setComment', payload);
 | 
			
		||||
    },
 | 
			
		||||
    fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
 | 
			
		||||
      fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
 | 
			
		||||
        .then(households => {
 | 
			
		||||
          commit('setHouseholdSuggestionByAccompanyingPeriod', households);
 | 
			
		||||
        });
 | 
			
		||||
    },
 | 
			
		||||
    computeWarnings({ commit, state, getters }) {
 | 
			
		||||
      let warnings = [],
 | 
			
		||||
        payload;
 | 
			
		||||
@@ -255,7 +303,7 @@ const store = createStore({
 | 
			
		||||
            if (household.type === 'household') {
 | 
			
		||||
              household_id = household.id;
 | 
			
		||||
              // nothing to do anymore here, bye-bye !
 | 
			
		||||
              window.location.replace(`/fr/person/household/${household_id}/members`);
 | 
			
		||||
              window.location.replace(`/fr/person/household/${household_id}/summary`);
 | 
			
		||||
            } else {
 | 
			
		||||
              // we assume the answer was 422...
 | 
			
		||||
              error = household;
 | 
			
		||||
@@ -274,4 +322,10 @@ const store = createStore({
 | 
			
		||||
 | 
			
		||||
store.dispatch('computeWarnings');
 | 
			
		||||
 | 
			
		||||
if (concerned.length > 0) {
 | 
			
		||||
  concerned.forEach(c => {
 | 
			
		||||
    store.dispatch('fetchHouseholdSuggestionForConcerned', c.person);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { store };
 | 
			
		||||
 
 | 
			
		||||
@@ -39,12 +39,15 @@
 | 
			
		||||
            <ul>
 | 
			
		||||
              {% for p in withoutHousehold %}
 | 
			
		||||
              <li>
 | 
			
		||||
                <input type="checkbox" name="persons[]" value="{{ p.id }}" />
 | 
			
		||||
                <input type="checkbox" name="persons[]" value="{{ p.id }}" checked />
 | 
			
		||||
                {{ p|chill_entity_render_box }}
 | 
			
		||||
              </li>
 | 
			
		||||
              {% endfor %}
 | 
			
		||||
            </ul>
 | 
			
		||||
 | 
			
		||||
            <input type="hidden" name="expand_suggestions" value="true" />
 | 
			
		||||
            <input type="hidden" name="accompanying_period_id", value="{{ accompanyingCourse.id }}" />
 | 
			
		||||
 | 
			
		||||
            <ul class="record_actions">
 | 
			
		||||
              <li>
 | 
			
		||||
                <button type="submit" class="sc-button bt-edit">
 | 
			
		||||
@@ -222,11 +225,107 @@
 | 
			
		||||
 | 
			
		||||
    <h2>{{ 'Social actions'|trans }}</h2>
 | 
			
		||||
 | 
			
		||||
    {% set person = null %}
 | 
			
		||||
    <div id="accompanying_course_work_list">
 | 
			
		||||
      {% for w in works %}
 | 
			
		||||
      <div class="item">
 | 
			
		||||
          <div class="title">
 | 
			
		||||
            <h2 class="action_title">
 | 
			
		||||
              {{ w.socialAction|chill_entity_render_box({ 'no-badge': false }) }}
 | 
			
		||||
            </h2>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <div class="timeline">
 | 
			
		||||
            <ul class="timeline">
 | 
			
		||||
              <li class="completed">
 | 
			
		||||
                <div class="date">
 | 
			
		||||
                  <span>{{ w.createdAt|format_date('long') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span>{{ 'accompanying_course_work.create_date'|trans }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li class="completed">
 | 
			
		||||
                <div class="date">
 | 
			
		||||
                  <span>{{ w.startDate|format_date('long') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span>{{ 'accompanying_course_work.start_date'|trans }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </li>
 | 
			
		||||
              <li class="{%if date(w.endDate) < date('now') %}completed{% endif %}">
 | 
			
		||||
                <div class="date">
 | 
			
		||||
                  <span>{{ w.endDate|format_date('long') }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="label">
 | 
			
		||||
                  <span>{{ 'accompanying_course_work.end_date'|trans }}</span>
 | 
			
		||||
                </div>
 | 
			
		||||
              </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          {% if w.results|length > 0 %}
 | 
			
		||||
            <div class="objective_results objective_results__without-objectives">
 | 
			
		||||
              <div class="obj without_objective">
 | 
			
		||||
                <p class="title_label">{{ 'accompanying_course_work.goal'|trans }}</p>
 | 
			
		||||
                <p class="chill-no-data-statement">{{ 'accompanying_course_work.results without objective'|trans }}</p>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div class="res results">
 | 
			
		||||
                <p class="title_label">{{ 'accompanying_course_work.results'|trans }}</p>
 | 
			
		||||
                <ul class="result_list">
 | 
			
		||||
                  {% for r in w.results %}
 | 
			
		||||
                    <li class="badge badge-primary">{{ r.title|localize_translatable_string }}</li>
 | 
			
		||||
                  {% endfor %}
 | 
			
		||||
                </ul>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          {% endif %}
 | 
			
		||||
 | 
			
		||||
          {% if w.goals|length > 0 %}
 | 
			
		||||
            {% for g in w.goals %}
 | 
			
		||||
              <div class="objective_results objective_results--with-objectives">
 | 
			
		||||
                <div class="objective obj">
 | 
			
		||||
                  <p class="title_label">{{ 'accompanying_course_work.goal'|trans }}</p>
 | 
			
		||||
                  <h3 class="goal_title">{{ g.goal.title|localize_translatable_string }}</h3>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div class="results res">
 | 
			
		||||
                  {% if g.results|length == 0 %}
 | 
			
		||||
                    <p class="title_label">{{ 'accompanying_course_work.results'|trans }}</p>
 | 
			
		||||
                    <p class="chill-no-data-statement">{{ 'accompanying_course_work.no_results'|trans }}</p>
 | 
			
		||||
                  {% else %}
 | 
			
		||||
                    <p class="title_label">{{ 'accompanying_course_work.results'|trans }}</p>
 | 
			
		||||
                    <ul class="result_list">
 | 
			
		||||
                      {% for r in g.results %}
 | 
			
		||||
                        <li class="badge badge-primary">{{ r.title|localize_translatable_string }}</li>
 | 
			
		||||
                      {% endfor %}
 | 
			
		||||
                    </ul>
 | 
			
		||||
                  {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            {% endfor %}
 | 
			
		||||
          {% endif %}
 | 
			
		||||
 | 
			
		||||
          <div class="updatedBy">
 | 
			
		||||
            {{ 'Last updated by'|trans}}: {{ w.updatedBy|chill_entity_render_box }}, {{ w.updatedAt|format_datetime('long', 'short') }}
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
      </div>
 | 
			
		||||
      {% else %}
 | 
			
		||||
        <p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>    
 | 
			
		||||
      {% endfor %}
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    {% block contentActivity %}
 | 
			
		||||
        {% set person = null %}
 | 
			
		||||
        {% include 'ChillActivityBundle:Activity:list.html.twig' with {'context': 'accompanyingCourse', 'context': 'person'} %}
 | 
			
		||||
    {% endblock %}
 | 
			
		||||
 | 
			
		||||
    {#  ==> insert accompanyingCourse vue component  #}
 | 
			
		||||
    <div id="accompanying-course"></div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
  {{ parent() }}
 | 
			
		||||
  {{ encore_entry_link_tags('accompanying_course_work_list') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,7 @@
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    {% else %}
 | 
			
		||||
      <p class="chill-no-data-statement">{{ 'accompanying_course_work.No work'|trans }}</p>
 | 
			
		||||
      <p class="chill-no-data-statement">{{ 'accompanying_course_work.Any work'|trans }}</p>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
   </div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,22 +8,28 @@
 | 
			
		||||
 | 
			
		||||
<h1>{{ 'Accompanying period list'|trans }}</h1>
 | 
			
		||||
 | 
			
		||||
<table class="rounded">
 | 
			
		||||
    <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th class="chill-red">{{ 'accompanying_period.dates'|trans }}</th>
 | 
			
		||||
            {% if chill_accompanying_periods.fields.user == 'visible'  %}
 | 
			
		||||
                <th class="chill-blue">{{ 'Accompanying user'|trans }}</th>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <th class="chill-orange">{{ 'Remark'|trans }}</th>
 | 
			
		||||
            <th> </th>
 | 
			
		||||
        </tr>
 | 
			
		||||
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {% for accompanying_period in accompanying_periods %}
 | 
			
		||||
        <tr>
 | 
			
		||||
            <td>
 | 
			
		||||
    {% for accompanying_period in accompanying_periods %}
 | 
			
		||||
 | 
			
		||||
    <div class="flex-table">
 | 
			
		||||
        <div class="item-bloc">
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
                <div class="item-col">
 | 
			
		||||
                    {{'Accompanying period'|trans}} #{{ accompanying_period.id }}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="item-col">
 | 
			
		||||
                {% if chill_accompanying_periods.fields.user == 'visible'  %}
 | 
			
		||||
                    {% if accompanying_period.user %}
 | 
			
		||||
                    {{ accompanying_period.user.username }}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
                {% if accompanying_period.closingDate == null %}
 | 
			
		||||
                    {{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }}
 | 
			
		||||
                {% else %}
 | 
			
		||||
@@ -32,41 +38,51 @@
 | 
			
		||||
                        '%closing_date%': accompanying_period.closingDate|format_date('long')}
 | 
			
		||||
                    ) }}
 | 
			
		||||
 | 
			
		||||
                {% if accompanying_period.isOpen == false %}
 | 
			
		||||
                    <dl class="chill_view_data">
 | 
			
		||||
                        <dt>{{ 'Closing motive'|trans }} :</dt>
 | 
			
		||||
                        <dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
 | 
			
		||||
                    </dl>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
                    {% if accompanying_period.isOpen == false %}
 | 
			
		||||
                        <dl class="chill_view_data">
 | 
			
		||||
                            <dt>{{ 'Closing motive'|trans }} :</dt>
 | 
			
		||||
                            <dd>{{ accompanying_period.closingMotive|chill_entity_render_box }}</dd>
 | 
			
		||||
                        </dl>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            {% if chill_accompanying_periods.fields.user == 'visible'  %}
 | 
			
		||||
            <td>
 | 
			
		||||
                {% if accompanying_period.user %}
 | 
			
		||||
                {{ accompanying_period.user.username }}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
 | 
			
		||||
                    <h3>{{ 'Participants'|trans }}</h3>
 | 
			
		||||
                    {% if accompanying_period.participations.count > 0 %}
 | 
			
		||||
                        {% for p in accompanying_period.participations %}
 | 
			
		||||
                        <p>
 | 
			
		||||
                            <a href="{{ path('chill_person_accompanying_period_list', { person_id: p.person.id }) }}">
 | 
			
		||||
                                {{ p.person.firstname ~ ' ' ~ p.person.lastname }}
 | 
			
		||||
                            </a>
 | 
			
		||||
                        </p>
 | 
			
		||||
                        {% endfor %}
 | 
			
		||||
                    {% else %}
 | 
			
		||||
                        <span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="item-col">
 | 
			
		||||
                <h3>{{ 'Requestors'|trans }}</h3>
 | 
			
		||||
                {% if accompanying_period.requestorPerson is not null or accompanying_period.requestorThirdParty is not null %}
 | 
			
		||||
                    {% if accompanying_period.requestorPerson is not null %}
 | 
			
		||||
                    <p>{{ accompanying_period.requestorPerson.firstname ~ ' ' ~ accompanying_period.requestorPerson.lastname }}</p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    {% if accompanying_period.requestorThirdParty is not null %}
 | 
			
		||||
                      <p>{{ accompanying_period.requestorThirdParty.name }}</p>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    <span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
 | 
			
		||||
                    <span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            {% endif %}
 | 
			
		||||
            <td>
 | 
			
		||||
                {% if accompanying_period is not empty %}
 | 
			
		||||
                    <blockquote class="chill-user-quote">
 | 
			
		||||
                    {{ accompanying_period.remark|chill_markdown_to_html }}
 | 
			
		||||
                    </blockquote>
 | 
			
		||||
                {% else %}
 | 
			
		||||
                    {{ null|chill_print_or_message('No remark', 'blockquote') }}
 | 
			
		||||
                {% endif %}
 | 
			
		||||
            </td>
 | 
			
		||||
            <td>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div class="item-row">
 | 
			
		||||
                <ul class="record_actions">
 | 
			
		||||
 | 
			
		||||
                    {# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}" class="sc-button bt-show"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
 | 
			
		||||
                    {#
 | 
			
		||||
                    <li>
 | 
			
		||||
                        <a  href="{{ path('chill_person_accompanying_period_update', {'person_id' : person.id, 'period_id' : accompanying_period.id } ) }}" class="sc-button bt-update no-content"></a>
 | 
			
		||||
                    </li>
 | 
			
		||||
@@ -84,21 +100,29 @@
 | 
			
		||||
                        </a>
 | 
			
		||||
                    </li>
 | 
			
		||||
                    {% endif %}
 | 
			
		||||
                    #}
 | 
			
		||||
                </ul>
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            </td>
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
    </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <p></p>
 | 
			
		||||
    {% endfor %}
 | 
			
		||||
 | 
			
		||||
<div class="form_control">
 | 
			
		||||
 | 
			
		||||
<div  class="form_control">
 | 
			
		||||
    <ul class="record_actions">
 | 
			
		||||
        <li class="cancel">
 | 
			
		||||
            <a href="{{ path ('chill_person_view', {'person_id' : person.id } ) }}" class="sc-button bt-cancel">
 | 
			
		||||
                {{ 'Person details'|trans }}
 | 
			
		||||
            </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        <li>
 | 
			
		||||
            <a href="{{ path ('chill_person_accompanying_course_new', {'person_id' : [ person.id ] } ) }}" class="sc-button bt-create">
 | 
			
		||||
              {{ 'Create an accompanying period'|trans }}
 | 
			
		||||
            </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        {#
 | 
			
		||||
        <li>
 | 
			
		||||
            <a href="{{ path ('chill_person_accompanying_period_create', {'person_id' : person.id } ) }}" class="sc-button bt-create has-hidden">
 | 
			
		||||
                <span class="show-on-hover">{{ 'Add an accompanying period in the past'|trans }}</span>
 | 
			
		||||
@@ -112,6 +136,7 @@
 | 
			
		||||
            </a>
 | 
			
		||||
        </li>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
        #}
 | 
			
		||||
    </ul>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -30,19 +30,19 @@
 | 
			
		||||
            <div id="banner-misc">
 | 
			
		||||
              {%- set members = household.getCurrentMembersOrdered() -%}
 | 
			
		||||
              {%- if members|length > 0 -%}
 | 
			
		||||
              <span class="current-members-explain">
 | 
			
		||||
              {{- 'household.Current household members'|trans }}:
 | 
			
		||||
              </span>
 | 
			
		||||
              {%- for m in members|slice(0, 5) -%}
 | 
			
		||||
              {{- m.person|chill_entity_render_box({'addLink': false}) -}}
 | 
			
		||||
              {%- if m.holder %} <span class="badge badge-primary">{{ 'household.holder'|trans }}</span>{% endif -%}
 | 
			
		||||
              {%- if false == loop.last -%}, {% endif -%}
 | 
			
		||||
              {%- endfor -%}
 | 
			
		||||
              {% if members|length > 5 %}
 | 
			
		||||
              <span class="current-members-more">
 | 
			
		||||
              {{ 'household.and x other persons'|trans({'x': members|length-5}) }}
 | 
			
		||||
              </span>
 | 
			
		||||
              {% endif %} 
 | 
			
		||||
                <span class="current-members-explain">
 | 
			
		||||
                {{- 'household.Current household members'|trans }}:
 | 
			
		||||
                </span>
 | 
			
		||||
                {%- for m in members|slice(0, 5) -%}
 | 
			
		||||
                  {{- m.person|chill_entity_render_box({'addLink': false}) -}}
 | 
			
		||||
                  {%- if m.holder %} <span class="badge badge-primary">{{ 'household.holder'|trans }}</span>{% endif -%}
 | 
			
		||||
                  {%- if false == loop.last -%}, {% endif -%}
 | 
			
		||||
                {%- endfor -%}
 | 
			
		||||
                  {% if members|length > 5 %}
 | 
			
		||||
                    <span class="current-members-more">
 | 
			
		||||
                    {{ 'household.and x other persons'|trans({'x': members|length-5}) }}
 | 
			
		||||
                    </span>
 | 
			
		||||
                  {% endif %} 
 | 
			
		||||
              {%- endif -%}
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
{% extends accompanyingCourse != null ? '@ChillPerson/AccompanyingCourse/layout.html.twig' 
 | 
			
		||||
  : '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block title 'household.Edit household members'|trans  %}
 | 
			
		||||
 | 
			
		||||
@@ -14,6 +15,7 @@
 | 
			
		||||
{% block js %}
 | 
			
		||||
  <script type="text/javascript">
 | 
			
		||||
    window.household_members_editor_data = {{ data|json_encode|raw }};
 | 
			
		||||
    window.household_members_editor_expand_suggestions = {{ expandSuggestions }};
 | 
			
		||||
  </script>
 | 
			
		||||
  {{ encore_entry_script_tags('household_members_editor') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,53 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\PersonBundle\Search;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonRepository;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiQuery;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiInterface;
 | 
			
		||||
 | 
			
		||||
class SearchPersonApiProvider implements SearchApiInterface
 | 
			
		||||
{
 | 
			
		||||
    private PersonRepository $personRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(PersonRepository $personRepository)
 | 
			
		||||
    {
 | 
			
		||||
        $this->personRepository = $personRepository;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public function provideQuery(string $pattern, array $parameters): SearchApiQuery
 | 
			
		||||
    {
 | 
			
		||||
        $query = new SearchApiQuery();
 | 
			
		||||
        $query
 | 
			
		||||
            ->setSelectKey("person")
 | 
			
		||||
            ->setSelectJsonbMetadata("jsonb_build_object('id', person.id)")
 | 
			
		||||
            ->setSelectPertinence("SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical)", [ $pattern ])
 | 
			
		||||
            ->setFromClause("chill_person_person AS person")
 | 
			
		||||
            ->setWhereClause("SIMILARITY(LOWER(UNACCENT(?)), person.fullnamecanonical) > 0.20", [ $pattern ])
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
        return $query;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsTypes(string $pattern, array $types, array $parameters): bool
 | 
			
		||||
    {
 | 
			
		||||
        return \in_array('person', $types);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function prepare(array $metadatas): void
 | 
			
		||||
    {
 | 
			
		||||
        $ids = \array_map(fn($m) => $m['id'], $metadatas);
 | 
			
		||||
 | 
			
		||||
        $this->personRepository->findByIds($ids);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsResult(string $key, array $metadatas): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $key === 'person';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResult(string $key, array $metadata, float $pertinence)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->personRepository->find($metadata['id']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,55 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\PersonBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HouseholdApiControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generatePersonId
 | 
			
		||||
     */
 | 
			
		||||
    public function testSuggestByAccompanyingPeriodParticipation(int $personId)
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
 | 
			
		||||
        $client->request(
 | 
			
		||||
            Request::METHOD_GET,
 | 
			
		||||
            "/api/1.0/person/household/suggest/by-person/{$personId}/through-accompanying-period-participation.json"
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertResponseIsSuccessful();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function generatePersonId()
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
 | 
			
		||||
        $qb = self::$container->get(EntityManagerInterface::class)
 | 
			
		||||
            ->createQueryBuilder();
 | 
			
		||||
 | 
			
		||||
        $period = $qb
 | 
			
		||||
            ->select('ap')
 | 
			
		||||
            ->from(AccompanyingPeriod::class, 'ap')
 | 
			
		||||
            ->where(
 | 
			
		||||
                $qb->expr()->gte('SIZE(ap.participations)', 2)
 | 
			
		||||
            )
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
            ->setMaxResults(1)
 | 
			
		||||
            ->getSingleResult()
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
        $person = $period->getParticipations()
 | 
			
		||||
            ->first()->getPerson(); 
 | 
			
		||||
 | 
			
		||||
        yield [ $person->getId() ];
 | 
			
		||||
    } 
 | 
			
		||||
}
 | 
			
		||||
@@ -967,6 +967,36 @@ paths:
 | 
			
		||||
        401:
 | 
			
		||||
          description: "Unauthorized"
 | 
			
		||||
 | 
			
		||||
  /1.0/person/household/suggest/by-person/{person_id}/through-accompanying-period-participation.json:
 | 
			
		||||
    get:
 | 
			
		||||
      tags:
 | 
			
		||||
        - household
 | 
			
		||||
      summary: Return households associated with the given person through accompanying periods
 | 
			
		||||
      description: |
 | 
			
		||||
          Return households associated with the given person throught accompanying periods participation.
 | 
			
		||||
 | 
			
		||||
          The current household of the given person is excluded.
 | 
			
		||||
      parameters:
 | 
			
		||||
        - name: person_id
 | 
			
		||||
          in: path
 | 
			
		||||
          required: true
 | 
			
		||||
          description: The person's id
 | 
			
		||||
          schema:
 | 
			
		||||
            type: integer
 | 
			
		||||
            format: integer
 | 
			
		||||
            minimum: 1
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: "ok"
 | 
			
		||||
          content:
 | 
			
		||||
            application/json:
 | 
			
		||||
              schema:
 | 
			
		||||
                $ref: '#/components/schemas/Household'
 | 
			
		||||
        404:
 | 
			
		||||
          description: "not found"
 | 
			
		||||
        401:
 | 
			
		||||
          description: "Unauthorized"
 | 
			
		||||
 | 
			
		||||
  /1.0/person/household/members/move.json:
 | 
			
		||||
    post:
 | 
			
		||||
      tags:
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,6 @@ module.exports = function(encore, entries)
 | 
			
		||||
    encore.addEntry('household_edit_metadata', __dirname + '/Resources/public/modules/household_edit_metadata/index.js');
 | 
			
		||||
    encore.addEntry('accompanying_course_work_create', __dirname + '/Resources/public/vuejs/AccompanyingCourseWorkCreate/index.js');
 | 
			
		||||
    encore.addEntry('accompanying_course_work_edit', __dirname + '/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js');
 | 
			
		||||
    encore.addEntry('accompanying_course_work_list', __dirname + '/Resources/public/modules/accompanying_course_work_list/index.js');
 | 
			
		||||
    encore.addEntry('person', __dirname + '/Resources/public/js/person.js');
 | 
			
		||||
    encore.addEntry('accompanying_course_work_list', __dirname + '/Resources/public/modules/accompanying_course_work_list/index.js');
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -46,6 +46,7 @@ services:
 | 
			
		||||
            $serializer: '@Symfony\Component\Serializer\SerializerInterface'
 | 
			
		||||
            $dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
 | 
			
		||||
            $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
 | 
			
		||||
            $workRepository: '@Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository'
 | 
			
		||||
        tags: ['controller.service_arguments']
 | 
			
		||||
 | 
			
		||||
    Chill\PersonBundle\Controller\AccompanyingCourseApiController:
 | 
			
		||||
 
 | 
			
		||||
@@ -28,3 +28,7 @@ services:
 | 
			
		||||
            $em: '@Doctrine\ORM\EntityManagerInterface'
 | 
			
		||||
            $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
 | 
			
		||||
            $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
 | 
			
		||||
 | 
			
		||||
    Chill\PersonBundle\Search\SearchPersonApiProvider:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 
 | 
			
		||||
@@ -165,6 +165,7 @@ An accompanying period starts: Une période d'accompagnement est ouverte
 | 
			
		||||
Any accompanying periods are open: Aucune période d'accompagnement ouverte
 | 
			
		||||
An accompanying period is open: Une période d'accompagnement est ouverte
 | 
			
		||||
Accompanying period list: Périodes d'accompagnement
 | 
			
		||||
Accompanying period: Période d'accompagnement
 | 
			
		||||
New accompanying course: Nouveau parcours d'accompagnement
 | 
			
		||||
Choose a motive: Motif de fermeture
 | 
			
		||||
Re-open accompanying period: Ré-ouvrir
 | 
			
		||||
@@ -353,4 +354,5 @@ accompanying_course_work:
 | 
			
		||||
    no_results: Aucun résultat - orientation
 | 
			
		||||
    results: Résultats - orientations
 | 
			
		||||
    goal: Objectif - motif - dispositif
 | 
			
		||||
    
 | 
			
		||||
    Any work: Aucune action d'accompagnement
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,48 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
namespace Chill\ThirdPartyBundle\Search;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiInterface;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiQuery;
 | 
			
		||||
use Chill\ThirdPartyBundle\Repository\ThirdPartyRepository;
 | 
			
		||||
 | 
			
		||||
class ThirdPartyApiSearch implements SearchApiInterface
 | 
			
		||||
{
 | 
			
		||||
    private ThirdPartyRepository $thirdPartyRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(ThirdPartyRepository $thirdPartyRepository)
 | 
			
		||||
    {
 | 
			
		||||
        $this->thirdPartyRepository = $thirdPartyRepository;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function provideQuery(string $pattern, array $parameters): SearchApiQuery
 | 
			
		||||
    {
 | 
			
		||||
        return (new SearchApiQuery)
 | 
			
		||||
            ->setSelectKey('tparty')
 | 
			
		||||
            ->setSelectJsonbMetadata("jsonb_build_object('id', tparty.id)")
 | 
			
		||||
            ->setSelectPertinence("SIMILARITY(?, LOWER(UNACCENT(tparty.name)))", [ $pattern ])
 | 
			
		||||
            ->setFromClause('chill_3party.third_party AS tparty')
 | 
			
		||||
            ->setWhereClause('SIMILARITY(LOWER(UNACCENT(?)), LOWER(UNACCENT(tparty.name))) > 0.20', [ $pattern ])
 | 
			
		||||
            ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsTypes(string $pattern, array $types, array $parameters): bool
 | 
			
		||||
    {
 | 
			
		||||
        return \in_array('thirdparty', $types);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function prepare(array $metadatas): void
 | 
			
		||||
    {
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function supportsResult(string $key, array $metadatas): bool
 | 
			
		||||
    {
 | 
			
		||||
        return $key === 'tparty';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getResult(string $key, array $metadata, float $pertinence)
 | 
			
		||||
    {
 | 
			
		||||
        return $this->thirdPartyRepository->find($metadata['id']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -7,3 +7,7 @@ services:
 | 
			
		||||
            $paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
 | 
			
		||||
        tags:
 | 
			
		||||
            - { name: 'chill.search', alias: '3party' }
 | 
			
		||||
 | 
			
		||||
    Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch:
 | 
			
		||||
        autowire: true
 | 
			
		||||
        autoconfigure: true
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user