Compare commits

...

34 Commits

Author SHA1 Message Date
014170989e Merge branch 'fix-prototypage-details' into deploy/quick-fixes 2021-06-29 15:21:00 +02:00
c26d342d30 [TO REVERSE] enlève menu activités 2021-06-29 00:19:57 +02:00
2cf28b6c2d fix in accompanying period list: thirdparty as requestor + link 2021-06-29 00:03:24 +02:00
ed5351da14 fix missing div in accompanying course work list 2021-06-28 23:48:06 +02:00
8dca8d9b37 Merge branch 'features/api-search' into deploy/quick-fixes 2021-06-28 23:21:36 +02:00
1cddad5c66 Merge remote-tracking branch 'origin/fix_issues_juminet' into deploy/quick-fixes 2021-06-28 23:09:22 +02:00
c98eda93cf Merge remote-tracking branch 'origin/fix-prototypage-details' into deploy/quick-fixes 2021-06-28 23:09:07 +02:00
dd991e3572 add test 2021-06-28 23:02:15 +02:00
0640631821 implementation on 3party + quote keys in SearchApiQuery 2021-06-28 22:44:42 +02:00
e845d9ba90 replace search api by a first workin implementation 2021-06-28 22:01:33 +02:00
nobohan
6f8231f6f6 accompanying course: clean controller 2021-06-28 22:01:05 +02:00
nobohan
048161e300 accompanying course summary (WIP) 2021-06-28 22:00:07 +02:00
nobohan
4f49292178 address: fix saving of isNoAddress 2021-06-28 17:18:57 +02:00
357bf192b8 Merge branch 'features/add-suggestion-household-member-editor' into deploy/quick-fixes 2021-06-28 16:22:36 +02:00
6eaffcae49 fix redirection after changing household member 2021-06-28 16:22:17 +02:00
nobohan
f92d710a26 accompanying course summary: add social actions 2021-06-28 16:16:55 +02:00
nobohan
bc6ba88acd menu: fix code indentation 2021-06-28 16:04:24 +02:00
efefb64119 Merge branch 'features/add-suggestion-household-member-editor' into deploy/quick-fixes 2021-06-28 15:51:37 +02:00
6fa882f60f Merge remote-tracking branch 'origin/fix_issues_juminet' into deploy/quick-fixes 2021-06-28 15:50:10 +02:00
484058dcfc Merge remote-tracking branch 'origin/master' into deploy/quick-fixes 2021-06-28 15:48:59 +02:00
nobohan
b72d45d9db accompanying period: hide activity entries from the menu if accompanyin period is DRAFT 2021-06-28 13:50:31 +02:00
2a1f5cbad1 add test for suggestion household by accompanying period 2021-06-28 13:18:00 +02:00
e8566fd006 change label 2021-06-28 12:36:19 +02:00
cd6b5c9a39 Add context for household member editor form:
* add context for accompanying period, if indicated in a query
parameter;
* expand automatically household suggestion, if indicated in a query
parameter
2021-06-28 11:23:30 +02:00
9aa3974071 show household suggestion in household members editor 2021-06-28 00:35:40 +02:00
a8bf478ee8 add endpoint for getting suggestion on household by accompanying period 2021-06-27 11:10:17 +02:00
a35f3363b2 Merge branch 'features/edit-accompanying-period-social-work' into 'master'
Features/edit accompanying period social work

See merge request Chill-Projet/chill-bundles!91
2021-06-26 13:15:34 +00:00
b7513068da Merge branch 'features/edit-accompanying-period-social-work' into deploy/quick-fixes 2021-06-25 15:01:46 +02:00
2dd71ea197 Merge remote-tracking branch 'origin/master' into deploy/quick-fixes 2021-06-25 15:00:24 +02:00
757c064b7b form acc work edit: save action 2021-06-25 00:05:27 +02:00
f5d38f606c Merge remote-tracking branch 'origin/fix_small_issues_juminet' into deploy/quick-fixes 2021-06-24 16:31:32 +02:00
5abe482b12 Merge remote-tracking branch 'origin/fix-prototypage-details' into deploy/quick-fixes 2021-06-24 16:30:50 +02:00
5ded4822a2 Merge branch 'features/edit-accompanying-period-social-work' into deploy/quick-fixes 2021-06-24 16:28:23 +02:00
6122a5d62f Merge remote-tracking branch 'origin/fix-prototypage-details' into deploy/quick-fixes 2021-06-23 10:30:33 +02:00
37 changed files with 1122 additions and 195 deletions

View File

@@ -4,6 +4,7 @@ namespace Chill\ActivityBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface; use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Knp\Menu\MenuItem; use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -34,21 +35,13 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
{ {
$period = $parameters['accompanyingCourse']; $period = $parameters['accompanyingCourse'];
if (AccompanyingPeriod::STEP_DRAFT !== $period->getStep()) {
$menu->addChild($this->translator->trans('Activity list'), [ $menu->addChild($this->translator->trans('Activity list'), [
'route' => 'chill_activity_activity_list', 'route' => 'chill_activity_activity_list',
'routeParameters' => [ 'routeParameters' => [
'accompanying_period_id' => $period->getId(), 'accompanying_period_id' => $period->getId(),
]]) ]])
->setExtras(['order' => 40]); ->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]);
} }
} }

View File

@@ -56,6 +56,7 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
/* @var $person \Chill\PersonBundle\Entity\Person */ /* @var $person \Chill\PersonBundle\Entity\Person */
$person = $parameters['person']; $person = $parameters['person'];
if (false) {
if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) { if ($this->authorizationChecker->isGranted(ActivityVoter::SEE, $person)) {
$menu->addChild( $menu->addChild(
$this->translator->trans('Activity list'), [ $this->translator->trans('Activity list'), [
@@ -75,6 +76,7 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
; ;
} }
} }
}
public static function getMenuIds(): array public static function getMenuIds(): array
{ {

View File

@@ -36,6 +36,7 @@ use Chill\MainBundle\Search\SearchProvider;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\SearchApi; use Chill\MainBundle\Search\SearchApi;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
/** /**
* Class SearchController * Class SearchController
@@ -151,11 +152,14 @@ class SearchController extends AbstractController
{ {
//TODO this is an incomplete implementation //TODO this is an incomplete implementation
$query = $request->query->get('q', ''); $query = $request->query->get('q', '');
$types = $request->query->get('type', []);
$results = $this->searchApi->getResults($query, 0, 150); if (count($types) === 0) {
$paginator = $this->paginatorFactory->create(count($results)); 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); return $this->json($collection);
} }

View File

@@ -132,6 +132,7 @@ class Address
/** /**
* True if the address is a "no address", aka homeless person, ... * True if the address is a "no address", aka homeless person, ...
* @groups({"write"}) * @groups({"write"})
* @ORM\Column(type="boolean")
* *
* @var bool * @var bool
*/ */
@@ -298,7 +299,7 @@ class Address
* @param bool $isNoAddress * @param bool $isNoAddress
* @return $this * @return $this
*/ */
public function setIsNoAddress(bool $isNoAddress) public function setIsNoAddress(bool $isNoAddress): self
{ {
$this->isNoAddress = $isNoAddress; $this->isNoAddress = $isNoAddress;
return $this; return $this;

View File

@@ -2,88 +2,169 @@
namespace Chill\MainBundle\Search; namespace Chill\MainBundle\Search;
use Chill\PersonBundle\Entity\Person; use Chill\MainBundle\Serializer\Model\Collection;
use Chill\ThirdPartyBundle\Entity\ThirdParty; 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\EntityManagerInterface;
use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Chill\MainBundle\Search\SearchProvider; use Chill\MainBundle\Search\SearchProvider;
use Symfony\Component\VarDumper\Resources\functions\dump; use Symfony\Component\VarDumper\Resources\functions\dump;
/** /**
* ***Warning*** This is an incomplete implementation ***Warning***
*/ */
class SearchApi class SearchApi
{ {
private EntityManagerInterface $em; 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->em = $em;
$this->search = $search; $this->providers[] = $searchPerson;
$this->providers[] = $thirdPartyApiSearch;
$this->paginator = $paginator;
} }
/** /**
* @return Model/Result[] * @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 $queries = $this->findQueries($pattern, $types, $parameters);
$results = [];
foreach ($this->getPersons($query) as $p) { $total = $this->countItems($queries, $types, $parameters);
$results[] = new Model\Result((float)\rand(0, 100) / 100, $p); $paginator = $this->paginator->create($total);
}
foreach ($this->getThirdParties($query) as $t) { $rawResults = $this->fetchRawResult($queries, $types, $parameters, $paginator);
$results[] = new Model\Result((float)\rand(0, 100) / 100, $t);
$this->prepareProviders($rawResults);
$results = $this->buildResults($rawResults);
$collection = new Collection($results, $paginator);
return $collection;
} }
\usort($results, function(Model\Result $a, Model\Result $b) { private function findQueries($pattern, array $types, array $parameters): array
return ($a->getRelevance() <=> $b->getRelevance()) * -1;
});
return $results;
}
public function countResults(string $query): int
{ {
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') return \array_filter(
->getScalarResult(); $this->providers,
$nbResults = rand(0, 15); fn($p) => $p->supportsTypes($pattern, $types, $parameters)
);
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;
} }
private function getPersons(string $query) private function countItems($providers, $types, $parameters): int
{ {
$params = [ list($countQuery, $parameters) = $this->buildCountQuery($providers, $types, $parameters);
SearchInterface::SEARCH_PREVIEW_OPTION => false $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 = [];
foreach ($queries as $q) {
$unions[] = $q->buildQuery();
$parameters = \array_merge($parameters, $q->buildParameters());
}
$unionUnordered = \implode(" UNION ", $unions);
return [
\strtr($query, [ '{union_unordered}' => $unionUnordered ]),
$parameters
]; ];
$search = $this->search->getResultByName($query, 'person_regular', 0, 50, $params, 'json');
$ids = \array_map(function($r) { return $r['id']; }, $search['results']);
if (count($ids) === 0) {
return [];
} }
return $this->em->getRepository(Person::class) private function buildUnionQuery(array $queries, $types, $parameters)
->findById($ids) {
$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 ?? [];
}
} }

View 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);
}

View 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
);
}
}

View 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;
}
}

View File

@@ -14,7 +14,7 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
public function normalize($address, string $format = null, array $context = []) public function normalize($address, string $format = null, array $context = [])
{ {
$data['address_id'] = $address->getId(); $data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getStreetNumber(); $data['text'] = $address->isNoAddress() ? '' : $address->getStreet().', '.$address->getStreetNumber();
$data['street'] = $address->getStreet(); $data['street'] = $address->getStreet();
$data['streetNumber'] = $address->getStreetNumber(); $data['streetNumber'] = $address->getStreetNumber();
$data['postcode']['name'] = $address->getPostCode()->getName(); $data['postcode']['name'] = $address->getPostCode()->getName();

View File

@@ -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'] ];
}
}

View File

@@ -5,6 +5,5 @@ services:
Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider' Chill\MainBundle\Search\SearchProvider: '@chill_main.search_provider'
Chill\MainBundle\Search\SearchApi: Chill\MainBundle\Search\SearchApi:
arguments: autowire: true
$em: '@Doctrine\ORM\EntityManagerInterface' autoconfigure: true
$search: '@Chill\MainBundle\Search\SearchProvider'

View File

@@ -7,6 +7,7 @@ use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent; use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller;
@@ -35,14 +36,18 @@ class AccompanyingCourseController extends Controller
protected ValidatorInterface $validator; protected ValidatorInterface $validator;
private AccompanyingPeriodWorkRepository $workRepository;
public function __construct( public function __construct(
SerializerInterface $serializer, SerializerInterface $serializer,
EventDispatcherInterface $dispatcher, EventDispatcherInterface $dispatcher,
ValidatorInterface $validator ValidatorInterface $validator,
AccompanyingPeriodWorkRepository $workRepository
) { ) {
$this->serializer = $serializer; $this->serializer = $serializer;
$this->dispatcher = $dispatcher; $this->dispatcher = $dispatcher;
$this->validator = $validator; $this->validator = $validator;
$this->workRepository = $workRepository;
} }
/** /**
@@ -102,9 +107,16 @@ class AccompanyingCourseController extends Controller
['date' => 'DESC'], ['date' => 'DESC'],
); );
$works = $this->workRepository->findByAccompanyingPeriod(
$accompanyingCourse,
['startDate' => 'DESC', 'endDate' => 'DESC'],
3
);
return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [ return $this->render('@ChillPerson/AccompanyingCourse/index.html.twig', [
'accompanyingCourse' => $accompanyingCourse, 'accompanyingCourse' => $accompanyingCourse,
'withoutHousehold' => $withoutHousehold, 'withoutHousehold' => $withoutHousehold,
'works' => $works,
'activities' => $activities 'activities' => $activities
]); ]);
} }

View File

@@ -75,8 +75,10 @@ class AccompanyingPeriodController extends AbstractController
]); ]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [ return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
'accompanying_periods' => $person->getAccompanyingPeriodsOrdered(), 'accompanying_periods' => $accompanyingPeriods,
'person' => $person 'person' => $person
]); ]);
} }

View File

@@ -4,15 +4,50 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address; 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\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
class HouseholdApiController extends ApiController class HouseholdApiController extends ApiController
{ {
private HouseholdRepository $householdRepository;
public function __construct(HouseholdRepository $householdRepository)
{
$this->householdRepository = $householdRepository;
}
public function householdAddressApi($id, Request $request, string $_format): Response public function householdAddressApi($id, Request $request, string $_format): Response
{ {
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]); 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"]]);
}
} }

View File

@@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Controller; namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
@@ -25,11 +26,17 @@ class HouseholdMemberController extends ApiController
private TranslatorInterface $translator; 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->generator = $generator;
$this->translator = $translator; $this->translator = $translator;
$this->periodRepository = $periodRepository;
} }
/** /**
@@ -144,8 +151,23 @@ class HouseholdMemberController extends ApiController
'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'), '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', [ return $this->render('@ChillPerson/Household/members_editor.html.twig', [
'data' => $data 'data' => $data,
'expandSuggestions' => (int) $request->query->getBoolean('expand_suggestions', false),
'accompanyingCourse' => $period ?? null,
]); ]);
} }

View File

@@ -565,6 +565,13 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
], ],
'controller_action' => 'householdAddressApi' 'controller_action' => 'householdAddressApi'
], ],
'suggestHouseholdByAccompanyingPeriodParticipation' => [
'path' => '/suggest/by-person/{person_id}/through-accompanying-period-participation.{_format}',
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
]
]
] ]
], ],
[ [

View File

@@ -25,8 +25,9 @@ namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodRepository final class AccompanyingPeriodRepository implements ObjectRepository
{ {
private EntityRepository $repository; private EntityRepository $repository;
@@ -34,4 +35,32 @@ final class AccompanyingPeriodRepository
{ {
$this->repository = $entityManager->getRepository(AccompanyingPeriod::class); $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;
}
} }

View File

@@ -3,15 +3,102 @@
namespace Chill\PersonBundle\Repository\Household; namespace Chill\PersonBundle\Repository\Household;
use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository; 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 EntityRepository $repository;
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $entityManager) public function __construct(EntityManagerInterface $entityManager)
{ {
$this->repository = $entityManager->getRepository(Household::class); $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;
}

View File

@@ -37,6 +37,11 @@ final class PersonRepository
return $this->repository->find($id, $lockMode, $lockVersion); return $this->repository->find($id, $lockMode, $lockVersion);
} }
public function findByIds($ids): array
{
return $this->repository->findBy(['id' => $ids]);
}
/** /**
* @param $centers * @param $centers
* @param $firstResult * @param $firstResult

View File

@@ -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 { export {
householdMove, householdMove,
fetchHouseholdSuggestionByAccompanyingPeriod,
}; };

View File

@@ -14,6 +14,23 @@
</div> </div>
<ul v-if="allowChangeHousehold" class="record_actions"> <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"> <li v-if="allowHouseholdCreate">
<button class="sc-button bt-create" @click="createHousehold"> <button class="sc-button bt-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }} {{ $t('household_members_editor.household.create_household') }}
@@ -31,11 +48,55 @@
</li> </li>
</ul> </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> </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> <script>
import { mapGetters } from 'vuex'; import { mapGetters, mapState } from 'vuex';
import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue'; import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue';
export default { export default {
@@ -47,6 +108,12 @@ export default {
...mapGetters([ ...mapGetters([
'hasHousehold', 'hasHousehold',
'isHouseholdNew', 'isHouseholdNew',
'hasHouseholdSuggestion',
'countHouseholdSuggestion',
'filterHouseholdSuggestionByAccompanyingPeriod',
]),
...mapState([
'showHouseholdSuggestion',
]), ]),
household() { household() {
return this.$store.state.household; return this.$store.state.household;
@@ -74,6 +141,12 @@ export default {
}, },
forceLeaveWithoutHousehold() { forceLeaveWithoutHousehold() {
this.$store.dispatch('forceLeaveWithoutHousehold'); this.$store.dispatch('forceLeaveWithoutHousehold');
},
toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion');
},
selectHousehold(h) {
this.$store.dispatch('selectHousehold', h);
} }
}, },
}; };

View File

@@ -5,7 +5,7 @@ const appMessages = {
fr: { fr: {
household_members_editor: { household_members_editor: {
household: { 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", new_household: "Nouveau ménage",
create_household: "Créer un ménage", create_household: "Créer un ménage",
search_household: "Chercher un ménage", search_household: "Chercher un ménage",
@@ -13,7 +13,7 @@ const appMessages = {
leave_without_household: "Sans nouveau ménage" leave_without_household: "Sans nouveau ménage"
}, },
concerned: { concerned: {
title: "Usagers concernés", title: "Nouveaux membres du ménage",
add_persons: "Ajouter d'autres usagers", add_persons: "Ajouter d'autres usagers",
search: "Rechercher des usagers", search: "Rechercher des usagers",
move_to: "Déplacer vers", move_to: "Déplacer vers",
@@ -29,6 +29,10 @@ const appMessages = {
remove_position: "Retirer des {position}", remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer", remove_concerned: "Ne plus transférer",
household_part: "Ménage de destination", 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_title: "Période de validité",
dates: { dates: {
start_date: "Début de validité", start_date: "Début de validité",

View File

@@ -1,5 +1,5 @@
import { createStore } from 'vuex'; import { createStore } from 'vuex';
import { householdMove, householdMoveTest } from './../api.js'; import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod } from './../api.js';
import { datetimeToISO } from 'ChillMainAssets/js/date.js'; import { datetimeToISO } from 'ChillMainAssets/js/date.js';
const debug = process.env.NODE_ENV !== 'production'; 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({ const store = createStore({
strict: debug, strict: debug,
state: { state: {
@@ -33,11 +35,16 @@ const store = createStore({
allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch,
allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold,
forceLeaveWithoutHousehold: false, forceLeaveWithoutHousehold: false,
householdSuggestionByAccompanyingPeriod: [],
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
warnings: [], warnings: [],
errors: [] errors: []
}, },
getters: { getters: {
isHouseholdNew(state) { isHouseholdNew(state) {
if (state.household === null) {
return false;
}
return !Number.isInteger(state.household.id); return !Number.isInteger(state.household.id);
}, },
hasHousehold(state) { hasHousehold(state) {
@@ -46,6 +53,21 @@ const store = createStore({
hasHouseholdOrLeave(state) { hasHouseholdOrLeave(state) {
return state.household !== null || state.forceLeaveWithoutHousehold; 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) { hasPersonsWellPositionnated(state, getters) {
return getters.needsPositionning === false return getters.needsPositionning === false
|| (getters.persons.length > 0 && getters.concUnpositionned.length === 0); || (getters.persons.length > 0 && getters.concUnpositionned.length === 0);
@@ -172,9 +194,25 @@ const store = createStore({
state.household = null; state.household = null;
state.forceLeaveWithoutHousehold = true; 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) { setStartDate(state, dateI) {
state.startDate = dateI; state.startDate = dateI;
}, },
toggleHouseholdSuggestion(state) {
state.showHouseholdSuggestion = !state.showHouseholdSuggestion;
},
setWarnings(state, warnings) { setWarnings(state, warnings) {
state.warnings = warnings; state.warnings = warnings;
// reset errors, which should come from servers // reset errors, which should come from servers
@@ -213,6 +251,10 @@ const store = createStore({
commit('forceLeaveWithoutHousehold'); commit('forceLeaveWithoutHousehold');
dispatch('computeWarnings'); dispatch('computeWarnings');
}, },
selectHousehold({ commit }, h) {
commit('selectHousehold', h);
dispatch('computeWarnings');
},
setStartDate({ commit, dispatch }, date) { setStartDate({ commit, dispatch }, date) {
commit('setStartDate', date); commit('setStartDate', date);
dispatch('computeWarnings'); dispatch('computeWarnings');
@@ -220,6 +262,12 @@ const store = createStore({
setComment({ commit }, payload) { setComment({ commit }, payload) {
commit('setComment', payload); commit('setComment', payload);
}, },
fetchHouseholdSuggestionForConcerned({ commit, state }, person) {
fetchHouseholdSuggestionByAccompanyingPeriod(person.id)
.then(households => {
commit('setHouseholdSuggestionByAccompanyingPeriod', households);
});
},
computeWarnings({ commit, state, getters }) { computeWarnings({ commit, state, getters }) {
let warnings = [], let warnings = [],
payload; payload;
@@ -255,7 +303,7 @@ const store = createStore({
if (household.type === 'household') { if (household.type === 'household') {
household_id = household.id; household_id = household.id;
// nothing to do anymore here, bye-bye ! // 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 { } else {
// we assume the answer was 422... // we assume the answer was 422...
error = household; error = household;
@@ -274,4 +322,10 @@ const store = createStore({
store.dispatch('computeWarnings'); store.dispatch('computeWarnings');
if (concerned.length > 0) {
concerned.forEach(c => {
store.dispatch('fetchHouseholdSuggestionForConcerned', c.person);
});
}
export { store }; export { store };

View File

@@ -39,12 +39,15 @@
<ul> <ul>
{% for p in withoutHousehold %} {% for p in withoutHousehold %}
<li> <li>
<input type="checkbox" name="persons[]" value="{{ p.id }}" /> <input type="checkbox" name="persons[]" value="{{ p.id }}" checked />
{{ p|chill_entity_render_box }} {{ p|chill_entity_render_box }}
</li> </li>
{% endfor %} {% endfor %}
</ul> </ul>
<input type="hidden" name="expand_suggestions" value="true" />
<input type="hidden" name="accompanying_period_id", value="{{ accompanyingCourse.id }}" />
<ul class="record_actions"> <ul class="record_actions">
<li> <li>
<button type="submit" class="sc-button bt-edit"> <button type="submit" class="sc-button bt-edit">
@@ -222,11 +225,107 @@
<h2>{{ 'Social actions'|trans }}</h2> <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 %} {% block contentActivity %}
{% set person = null %}
{% include 'ChillActivityBundle:Activity:list.html.twig' with {'context': 'accompanyingCourse', 'context': 'person'} %} {% include 'ChillActivityBundle:Activity:list.html.twig' with {'context': 'accompanyingCourse', 'context': 'person'} %}
{% endblock %} {% endblock %}
{# ==> insert accompanyingCourse vue component #} {# ==> insert accompanyingCourse vue component #}
<div id="accompanying-course"></div> <div id="accompanying-course"></div>
{% endblock %} {% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('accompanying_course_work_list') }}
{% endblock %}

View File

@@ -102,7 +102,7 @@
</div> </div>
</div> </div>
{% else %} {% 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 %} {% endfor %}
</div> </div>

View File

@@ -8,22 +8,28 @@
<h1>{{ 'Accompanying period list'|trans }}</h1> <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>&nbsp;</th>
</tr>
</thead>
<tbody>
{% for accompanying_period in accompanying_periods %} {% for accompanying_period in accompanying_periods %}
<tr>
<td> <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 %} {% if accompanying_period.closingDate == null %}
{{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }} {{ 'accompanying_period.dates_from_%opening_date%'|trans({ '%opening_date%': accompanying_period.openingDate|format_date('long') } ) }}
{% else %} {% else %}
@@ -40,33 +46,43 @@
{% endif %} {% endif %}
{% endif %} {% endif %}
</td> </div>
{% if chill_accompanying_periods.fields.user == 'visible' %} <div class="item-row">
<td>
{% if accompanying_period.user %}
{{ accompanying_period.user.username }}
{% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|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>
<ul class="record_actions">
<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 data given'|trans }}</span>
{% endif %}
</div>
<div class="item-row">
<ul class="record_actions">
{# TODO if enable_accompanying_course_with_multiple_persons is true ... #} {# TODO if enable_accompanying_course_with_multiple_persons is true ... #}
<li> <li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}" class="sc-button bt-show"></a> <a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': accompanying_period.id }) }}" class="sc-button bt-show"></a>
</li> </li>
{#
<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> <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> </li>
@@ -84,13 +100,15 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
#}
</ul> </ul>
</div>
</td> </div>
</tr> </div>
<p></p>
{% endfor %} {% endfor %}
</tbody>
</table>
<div class="form_control"> <div class="form_control">
<ul class="record_actions"> <ul class="record_actions">
@@ -99,6 +117,12 @@
{{ 'Person details'|trans }} {{ 'Person details'|trans }}
</a> </a>
</li> </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> <li>
<a href="{{ path ('chill_person_accompanying_period_create', {'person_id' : person.id } ) }}" class="sc-button bt-create has-hidden"> <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> <span class="show-on-hover">{{ 'Add an accompanying period in the past'|trans }}</span>
@@ -112,6 +136,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
#}
</ul> </ul>
</div> </div>

View File

@@ -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 %} {% block title 'household.Edit household members'|trans %}
@@ -14,6 +15,7 @@
{% block js %} {% block js %}
<script type="text/javascript"> <script type="text/javascript">
window.household_members_editor_data = {{ data|json_encode|raw }}; window.household_members_editor_data = {{ data|json_encode|raw }};
window.household_members_editor_expand_suggestions = {{ expandSuggestions }};
</script> </script>
{{ encore_entry_script_tags('household_members_editor') }} {{ encore_entry_script_tags('household_members_editor') }}
{% endblock %} {% endblock %}

View File

@@ -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']);
}
}

View File

@@ -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() ];
}
}

View File

@@ -967,6 +967,36 @@ paths:
401: 401:
description: "Unauthorized" 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: /1.0/person/household/members/move.json:
post: post:
tags: tags:

View File

@@ -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('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_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_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('person', __dirname + '/Resources/public/js/person.js');
encore.addEntry('accompanying_course_work_list', __dirname + '/Resources/public/modules/accompanying_course_work_list/index.js');
}; };

View File

@@ -46,6 +46,7 @@ services:
$serializer: '@Symfony\Component\Serializer\SerializerInterface' $serializer: '@Symfony\Component\Serializer\SerializerInterface'
$dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' $dispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
$workRepository: '@Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository'
tags: ['controller.service_arguments'] tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingCourseApiController: Chill\PersonBundle\Controller\AccompanyingCourseApiController:

View File

@@ -28,3 +28,7 @@ services:
$em: '@Doctrine\ORM\EntityManagerInterface' $em: '@Doctrine\ORM\EntityManagerInterface'
$tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface' $tokenStorage: '@Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface'
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'
Chill\PersonBundle\Search\SearchPersonApiProvider:
autowire: true
autoconfigure: true

View File

@@ -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 Any accompanying periods are open: Aucune période d'accompagnement ouverte
An accompanying period is open: Une période d'accompagnement est ouverte An accompanying period is open: Une période d'accompagnement est ouverte
Accompanying period list: Périodes d'accompagnement Accompanying period list: Périodes d'accompagnement
Accompanying period: Période d'accompagnement
New accompanying course: Nouveau parcours d'accompagnement New accompanying course: Nouveau parcours d'accompagnement
Choose a motive: Motif de fermeture Choose a motive: Motif de fermeture
Re-open accompanying period: Ré-ouvrir Re-open accompanying period: Ré-ouvrir
@@ -353,4 +354,5 @@ accompanying_course_work:
no_results: Aucun résultat - orientation no_results: Aucun résultat - orientation
results: Résultats - orientations results: Résultats - orientations
goal: Objectif - motif - dispositif goal: Objectif - motif - dispositif
Any work: Aucune action d'accompagnement

View File

@@ -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']);
}
}

View File

@@ -7,3 +7,7 @@ services:
$paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory' $paginatorFactory: '@Chill\MainBundle\Pagination\PaginatorFactory'
tags: tags:
- { name: 'chill.search', alias: '3party' } - { name: 'chill.search', alias: '3party' }
Chill\ThirdPartyBundle\Search\ThirdPartyApiSearch:
autowire: true
autoconfigure: true