mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-10-25 14:42:48 +00:00 
			
		
		
		
	Compare commits
	
		
			34 Commits
		
	
	
		
			362-bug-ma
			...
			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