diff --git a/CHANGELOG.md b/CHANGELOG.md
index c53bd97e4..22639a120 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,8 +12,14 @@ and this project adheres to
* [person] add validator for accompanying period with a test on social issues (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/76)
+* [main] address: use search API end points for getting postal code and reference address (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
+* [main] address: in edit mode, select the encoded values in multiselect for address reference and city (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/316)
* [person search] fix bug when using birthdate after and birthdate before
* [person search] increase pertinence when lastname begins with search pattern
+* [household] field to edit wheter person is titulaire of household or not removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/322)
+* [activity] create work if a work with same social action is not associated to the activity
+* [visgraph] improve and fix bugs on vis-network relationship graph
+* [bugfix] posting of birth- and deathdate through api fixed.
## Test releases
@@ -24,6 +30,7 @@ and this project adheres to
* [activity] layout for issues / actions
* [activity][bugfix] in edit mode, the form will now load the social action list
+
### Test release 2021-11-29
* [person] suggest entities (person | thirdparty) when creating/editing the accompanying course (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/119)
@@ -51,6 +58,9 @@ and this project adheres to
* [activity] for a new activity: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
* [calendar] for a new rdv: suggest and create on-the-fly locations based on the accompanying course location + location of the suggested parties
+
+## Test releases
+
### Test release 2021-11-22
* [activity] delete admin_user_show in twig template because this route is not defined and should be defined
diff --git a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php
index e1c657947..39c7eab36 100644
--- a/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php
+++ b/src/Bundle/ChillActivityBundle/DependencyInjection/ChillActivityExtension.php
@@ -41,6 +41,7 @@ class ChillActivityExtension extends Extension implements PrependExtensionInterf
$loader->load('services/form.yaml');
$loader->load('services/templating.yaml');
$loader->load('services/accompanyingPeriodConsistency.yaml');
+ $loader->load('services/doctrine.entitylistener.yaml');
}
public function prepend(ContainerBuilder $container)
diff --git a/src/Bundle/ChillActivityBundle/Entity/Activity.php b/src/Bundle/ChillActivityBundle/Entity/Activity.php
index 183b56225..77f650ce4 100644
--- a/src/Bundle/ChillActivityBundle/Entity/Activity.php
+++ b/src/Bundle/ChillActivityBundle/Entity/Activity.php
@@ -47,7 +47,8 @@ use Symfony\Component\Serializer\Annotation\SerializedName;
* })
* @ActivityValidator\ActivityValidity
*
- * @UserCircleConsistency(
+ * TODO see if necessary
+ * UserCircleConsistency(
* "CHILL_ACTIVITY_SEE_DETAILS",
* getUserFunction="getUser",
* path="scope")
diff --git a/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php
new file mode 100644
index 000000000..ab370e89e
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/EntityListener/ActivityEntityListener.php
@@ -0,0 +1,77 @@
+em = $em;
+ $this->workRepository = $workRepository;
+ }
+
+ public function persistActionToCourse(Activity $activity)
+ {
+ if ($activity->getAccompanyingPeriod() instanceof AccompanyingPeriod) {
+ $period = $activity->getAccompanyingPeriod();
+
+ $accompanyingCourseWorks = $this->workRepository->findByAccompanyingPeriod($period);
+ $periodActions = [];
+ $now = new DateTimeImmutable();
+
+ foreach ($accompanyingCourseWorks as $key => $work) {
+ // take only the actions which are still opened
+ if ($work->getEndDate() === null || $work->getEndDate() > ($activity->getDate() ?? $now)) {
+ $periodActions[$key] = spl_object_hash($work->getSocialAction());
+ }
+ }
+
+ $associatedPersons = $activity->getPersonsAssociated();
+ $associatedThirdparties = $activity->getThirdParties();
+
+ foreach ($activity->getSocialActions() as $action) {
+ if (in_array(spl_object_hash($action), $periodActions, true)) {
+ continue;
+ }
+ $newAction = new AccompanyingPeriodWork();
+ $newAction->setSocialAction($action);
+ $period->addWork($newAction);
+
+ $date = DateTimeImmutable::createFromMutable($activity->getDate());
+ $newAction->setStartDate($date);
+
+ foreach ($associatedPersons as $person) {
+ $newAction->addPerson($person);
+ }
+
+ foreach ($associatedThirdparties as $thirdparty) {
+ $newAction->setHandlingThierparty($thirdparty);
+ }
+ $this->em->persist($newAction);
+ $this->em->flush();
+ }
+ }
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php
index 0955a26c2..eec801be5 100644
--- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php
+++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php
@@ -143,7 +143,7 @@ class ActivityType extends AbstractType
return array_map(
fn (string $id): ?SocialIssue => $this->om->getRepository(SocialIssue::class)->findOneBy(['id' => (int) $id]),
- explode(',', $socialIssuesAsString)
+ explode(',', (string) $socialIssuesAsString)
);
}
));
@@ -169,7 +169,7 @@ class ActivityType extends AbstractType
return array_map(
fn (string $id): ?SocialAction => $this->om->getRepository(SocialAction::class)->findOneBy(['id' => (int) $id]),
- explode(',', $socialActionsAsString)
+ explode(',', (string) $socialActionsAsString)
);
}
));
@@ -266,7 +266,7 @@ class ActivityType extends AbstractType
return array_map(
fn (string $id): ?Person => $this->om->getRepository(Person::class)->findOneBy(['id' => (int) $id]),
- explode(',', $personsAsString)
+ explode(',', (string) $personsAsString)
);
}
));
@@ -292,7 +292,7 @@ class ActivityType extends AbstractType
return array_map(
fn (string $id): ?ThirdParty => $this->om->getRepository(ThirdParty::class)->findOneBy(['id' => (int) $id]),
- explode(',', $thirdpartyAsString)
+ explode(',', (string) $thirdpartyAsString)
);
}
));
@@ -329,7 +329,7 @@ class ActivityType extends AbstractType
return array_map(
fn (string $id): ?User => $this->om->getRepository(User::class)->findOneBy(['id' => (int) $id]),
- explode(',', $usersAsString)
+ explode(',', (string) $usersAsString)
);
}
));
@@ -344,7 +344,7 @@ class ActivityType extends AbstractType
return '';
}
- return $location->getId();
+ return (string) $location->getId();
},
function (?string $id): ?Location {
return $this->om->getRepository(Location::class)->findOneBy(['id' => (int) $id]);
diff --git a/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml b/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml
new file mode 100644
index 000000000..3c439153b
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/config/services/doctrine.entitylistener.yaml
@@ -0,0 +1,15 @@
+services:
+ Chill\ActivityBundle\EntityListener\ActivityEntityListener:
+ autowire: true
+ autoconfigure: true
+ tags:
+ -
+ name: 'doctrine.orm.entity_listener'
+ event: 'postPersist'
+ entity: 'Chill\ActivityBundle\Entity\Activity'
+ method: 'persistActionToCourse'
+ -
+ name: 'doctrine.orm.entity_listener'
+ event: 'postUpdate'
+ entity: 'Chill\ActivityBundle\Entity\Activity'
+ method: 'persistActionToCourse'
diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php
index 1230cd0f6..498c77c68 100644
--- a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php
+++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php
@@ -12,14 +12,66 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
+use Chill\MainBundle\Entity\PostalCode;
+use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Pagination\PaginatorInterface;
+use Chill\MainBundle\Repository\AddressReferenceRepository;
+use Chill\MainBundle\Serializer\Model\Collection;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
+use function trim;
-/**
- * Class AddressReferenceAPIController.
- */
-class AddressReferenceAPIController extends ApiController
+final class AddressReferenceAPIController extends ApiController
{
+ private AddressReferenceRepository $addressReferenceRepository;
+
+ private PaginatorFactory $paginatorFactory;
+
+ public function __construct(AddressReferenceRepository $addressReferenceRepository, PaginatorFactory $paginatorFactory)
+ {
+ $this->addressReferenceRepository = $addressReferenceRepository;
+ $this->paginatorFactory = $paginatorFactory;
+ }
+
+ /**
+ * @Route("/api/1.0/main/address-reference/by-postal-code/{id}/search.json")
+ */
+ public function search(PostalCode $postalCode, Request $request): JsonResponse
+ {
+ $this->denyAccessUnlessGranted('ROLE_USER');
+
+ if (!$request->query->has('q')) {
+ throw new BadRequestHttpException('You must supply a "q" parameter');
+ }
+
+ $pattern = $request->query->get('q');
+
+ if ('' === trim($pattern)) {
+ throw new BadRequestHttpException('the search pattern is empty');
+ }
+
+ $nb = $this->addressReferenceRepository->countByPostalCodePattern($postalCode, $pattern);
+ $paginator = $this->paginatorFactory->create($nb);
+ $addresses = $this->addressReferenceRepository->findByPostalCodePattern(
+ $postalCode,
+ $pattern,
+ false,
+ $paginator->getCurrentPageFirstItemNumber(),
+ $paginator->getItemsPerPage()
+ );
+
+ return $this->json(
+ new Collection($addresses, $paginator),
+ Response::HTTP_OK,
+ [],
+ [AbstractNormalizer::GROUPS => ['read']]
+ );
+ }
+
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('postal_code')) {
diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php
index fb926733b..fa1a29296 100644
--- a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php
+++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php
@@ -12,13 +12,80 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
+use Chill\MainBundle\Pagination\PaginatorFactory;
+use Chill\MainBundle\Repository\CountryRepository;
+use Chill\MainBundle\Repository\PostalCodeRepository;
+use Chill\MainBundle\Serializer\Model\Collection;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
+use Symfony\Component\Routing\Annotation\Route;
+use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
-/**
- * Class PostalCodeAPIController.
- */
-class PostalCodeAPIController extends ApiController
+final class PostalCodeAPIController extends ApiController
{
+ private CountryRepository $countryRepository;
+
+ private PaginatorFactory $paginatorFactory;
+
+ private PostalCodeRepository $postalCodeRepository;
+
+ public function __construct(
+ CountryRepository $countryRepository,
+ PostalCodeRepository $postalCodeRepository,
+ PaginatorFactory $paginatorFactory
+ ) {
+ $this->countryRepository = $countryRepository;
+ $this->postalCodeRepository = $postalCodeRepository;
+ $this->paginatorFactory = $paginatorFactory;
+ }
+
+ /**
+ * @Route("/api/1.0/main/postal-code/search.json")
+ */
+ public function search(Request $request): JsonResponse
+ {
+ $this->denyAccessUnlessGranted('ROLE_USER');
+
+ if (!$request->query->has('q')) {
+ throw new BadRequestHttpException('You must supply a "q" parameter');
+ }
+
+ $pattern = $request->query->get('q');
+
+ if ('' === trim($pattern)) {
+ throw new BadRequestHttpException('the search pattern is empty');
+ }
+
+ if ($request->query->has('country')) {
+ $country = $this->countryRepository->find($request->query->getInt('country'));
+
+ if (null === $country) {
+ throw new NotFoundHttpException('country not found');
+ }
+ } else {
+ $country = null;
+ }
+
+ $nb = $this->postalCodeRepository->countByPattern($pattern, $country);
+ $paginator = $this->paginatorFactory->create($nb);
+ $codes = $this->postalCodeRepository->findByPattern(
+ $pattern,
+ $country,
+ $paginator->getCurrentPageFirstItemNumber(),
+ $paginator->getItemsPerPage()
+ );
+
+ return $this->json(
+ new Collection($codes, $paginator),
+ Response::HTTP_OK,
+ [],
+ [AbstractNormalizer::GROUPS => ['read']]
+ );
+ }
+
protected function customizeQuery(string $action, Request $request, $qb): void
{
if ($request->query->has('country')) {
diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php
index e793167f6..99efd391d 100644
--- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php
+++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php
@@ -22,6 +22,15 @@ use Symfony\Component\Serializer\Annotation\Groups;
*/
class AddressReference
{
+ /**
+ * This is an internal column which is populated by database.
+ *
+ * This column will ease the search operations
+ *
+ * @ORM\Column(type="text", options={"default": ""})
+ */
+ private string $addressCanonical = '';
+
/**
* @ORM\Id
* @ORM\GeneratedValue
diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php
index 866ad04db..484a9e322 100644
--- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php
+++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php
@@ -29,6 +29,15 @@ use Symfony\Component\Serializer\Annotation\Groups;
*/
class PostalCode
{
+ /**
+ * This is an internal column which is populated by database.
+ *
+ * This column will ease the search operations
+ *
+ * @ORM\Column(type="text", options={"default": ""})
+ */
+ private string $canonical = '';
+
/**
* @var Point
*
diff --git a/src/Bundle/ChillMainBundle/Pagination/Paginator.php b/src/Bundle/ChillMainBundle/Pagination/Paginator.php
index d6da55618..54fa36084 100644
--- a/src/Bundle/ChillMainBundle/Pagination/Paginator.php
+++ b/src/Bundle/ChillMainBundle/Pagination/Paginator.php
@@ -109,6 +109,10 @@ class Paginator implements PaginatorInterface
return 1;
}
+ if (0 === $this->totalItems) {
+ return 1;
+ }
+
$nb = floor($this->totalItems / $this->itemPerPage);
if ($this->totalItems % $this->itemPerPage > 0) {
@@ -211,6 +215,10 @@ class Paginator implements PaginatorInterface
public function hasPage($number)
{
+ if (0 === $this->totalItems) {
+ return 1 === $number;
+ }
+
return 0 < $number
&& $this->countPages() >= $number;
}
diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php
index 4344964af..6be52ec85 100644
--- a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php
@@ -12,17 +12,29 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\AddressReference;
+use Chill\MainBundle\Entity\PostalCode;
+use Chill\MainBundle\Search\SearchApiQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\Query\ResultSetMapping;
+use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
+use RuntimeException;
+use function explode;
+use function implode;
+use function strtr;
+use function trim;
final class AddressReferenceRepository implements ObjectRepository
{
+ private EntityManagerInterface $entityManager;
+
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(AddressReference::class);
+ $this->entityManager = $entityManager;
}
public function countAll(): int
@@ -33,6 +45,18 @@ final class AddressReferenceRepository implements ObjectRepository
return $qb->getQuery()->getSingleScalarResult();
}
+ public function countByPostalCodePattern(PostalCode $postalCode, string $pattern): int
+ {
+ $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern);
+ $sql = $query->buildQuery(true);
+ $rsm = new ResultSetMapping();
+ $rsm->addScalarResult('c', 'c');
+
+ $nq = $this->entityManager->createNativeQuery($sql, $rsm)->setParameters($query->buildParameters(true));
+
+ return (int) $nq->getSingleResult()['c'];
+ }
+
public function find($id, $lockMode = null, $lockVersion = null): ?AddressReference
{
return $this->repository->find($id, $lockMode, $lockVersion);
@@ -57,6 +81,33 @@ final class AddressReferenceRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
+ /**
+ * @return AddressReference[]|array
+ */
+ public function findByPostalCodePattern(PostalCode $postalCode, string $pattern, bool $simplify = false, int $start = 0, int $limit = 50): array
+ {
+ $query = $this->buildQueryByPostalCodePattern($postalCode, $pattern);
+
+ if (!$simplify) {
+ $rsm = new ResultSetMappingBuilder($this->entityManager);
+ $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'cma');
+ $query->addSelectClause($rsm->generateSelectClause());
+ } else {
+ throw new RuntimeException('not implemented');
+ }
+
+ $sql = strtr(
+ $query->buildQuery() . 'ORDER BY pertinence DESC, lpad(streetnumber, 10, \'0\') ASC OFFSET ? LIMIT ? ',
+ // little hack for adding sql method to point
+ ['cma.point AS point' => 'ST_AsGeojson(cma.point) AS point']
+ );
+ $parameters = [...$query->buildParameters(), $start, $limit];
+
+ return $this->entityManager->createNativeQuery($sql, $rsm)
+ ->setParameters($parameters)
+ ->getResult();
+ }
+
public function findOneBy(array $criteria, ?array $orderBy = null): ?AddressReference
{
return $this->repository->findOneBy($criteria, $orderBy);
@@ -66,4 +117,44 @@ final class AddressReferenceRepository implements ObjectRepository
{
return AddressReference::class;
}
+
+ private function buildQueryByPostalCodePattern(PostalCode $postalCode, string $pattern): SearchApiQuery
+ {
+ $pattern = trim($pattern);
+
+ if ('' === $pattern) {
+ throw new RuntimeException('the search pattern must not be empty');
+ }
+ $query = new SearchApiQuery();
+
+ $query
+ ->setFromClause('chill_main_address_reference cma')
+ ->andWhereClause('postcode_id = ?', [$postalCode->getId()]);
+
+ $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))'];
+ $pertinenceArgs = [$pattern];
+ $orWhere = ['addresscanonical %>> UNACCENT(?)'];
+ $orWhereArgs = [$pattern];
+
+ foreach (explode(' ', $pattern) as $part) {
+ $part = trim($part);
+
+ if ('' === $part) {
+ continue;
+ }
+
+ $orWhere[] = "addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%'";
+ $orWhereArgs[] = $part;
+ $pertinenceClause[] =
+ "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
+ $pertinenceClause[] =
+ '(addresscanonical LIKE UNACCENT(LOWER(?)))::int';
+ array_push($pertinenceArgs, $part, $part);
+ }
+ $query
+ ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs)
+ ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs);
+
+ return $query;
+ }
}
diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php
index 697c8f90d..02e63771b 100644
--- a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php
@@ -11,18 +11,39 @@ declare(strict_types=1);
namespace Chill\MainBundle\Repository;
+use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\PostalCode;
+use Chill\MainBundle\Search\SearchApiQuery;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
+use Doctrine\ORM\Query\ResultSetMapping;
+use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Doctrine\Persistence\ObjectRepository;
+use RuntimeException;
final class PostalCodeRepository implements ObjectRepository
{
+ private EntityManagerInterface $entityManager;
+
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PostalCode::class);
+ $this->entityManager = $entityManager;
+ }
+
+ public function countByPattern(string $pattern, ?Country $country): int
+ {
+ $query = $this->buildQueryByPattern($pattern, $country);
+ $sql = $query->buildQuery(true);
+ $rsm = new ResultSetMapping();
+ $rsm->addScalarResult('c', 'c');
+
+ $nq = $this->entityManager->createNativeQuery($sql, $rsm)
+ ->setParameters($query->buildParameters(true));
+
+ return (int) $nq->getSingleResult()['c'];
}
public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode
@@ -49,6 +70,26 @@ final class PostalCodeRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
+ public function findByPattern(string $pattern, ?Country $country, ?int $start = 0, ?int $limit = 50): array
+ {
+ $query = $this->buildQueryByPattern($pattern, $country);
+
+ $rsm = new ResultSetMappingBuilder($this->entityManager);
+ $rsm->addRootEntityFromClassMetadata(PostalCode::class, 'cmpc');
+ $query->addSelectClause($rsm->generateSelectClause());
+
+ $sql = strtr(
+ $query->buildQuery() . 'ORDER BY pertinence DESC, canonical ASC OFFSET ? LIMIT ? ',
+ // little hack for adding sql method to point
+ ['cmpc.center AS center' => 'ST_AsGeojson(cmpc.center) AS center']
+ );
+ $parameters = [...$query->buildParameters(), $start, $limit];
+
+ return $this->entityManager->createNativeQuery($sql, $rsm)
+ ->setParameters($parameters)
+ ->getResult();
+ }
+
public function findOneBy(array $criteria, ?array $orderBy = null): ?PostalCode
{
return $this->repository->findOneBy($criteria, $orderBy);
@@ -58,4 +99,48 @@ final class PostalCodeRepository implements ObjectRepository
{
return PostalCode::class;
}
+
+ private function buildQueryByPattern(string $pattern, ?Country $country): SearchApiQuery
+ {
+ $pattern = trim($pattern);
+
+ if ('' === $pattern) {
+ throw new RuntimeException('the search pattern must not be empty');
+ }
+ $query = new SearchApiQuery();
+
+ $query
+ ->setFromClause('chill_main_postal_code cmpc')
+ ->andWhereClause('cmpc.origin = 0');
+
+ if (null !== $country) {
+ $query->andWhereClause('cmpc.country_id = ?', [$country->getId()]);
+ }
+
+ $pertinenceClause = ['STRICT_WORD_SIMILARITY(canonical, UNACCENT(?))'];
+ $pertinenceArgs = [$pattern];
+ $orWhere = ['canonical %>> UNACCENT(?)'];
+ $orWhereArgs = [$pattern];
+
+ foreach (explode(' ', $pattern) as $part) {
+ $part = trim($part);
+
+ if ('' === $part) {
+ continue;
+ }
+
+ $orWhere[] = "canonical LIKE '%' || UNACCENT(LOWER(?)) || '%'";
+ $orWhereArgs[] = $part;
+ $pertinenceClause[] =
+ "(EXISTS (SELECT 1 FROM unnest(string_to_array(canonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int";
+ $pertinenceClause[] =
+ '(canonical LIKE UNACCENT(LOWER(?)))::int';
+ array_push($pertinenceArgs, $part, $part);
+ }
+ $query
+ ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs)
+ ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs);
+
+ return $query;
+ }
}
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js
index 62065b3ad..1dbc85dee 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js
@@ -16,7 +16,8 @@ const fetchCountries = () => {
/**
* Endpoint chill_api_single_postal_code__index
-* method GET, get Country Object
+* method GET, get Cities Object
+* @params {object} a country object
* @returns {Promise} a promise containing all Postal Code objects filtered with country
*/
const fetchCities = (country) => {
@@ -29,6 +30,40 @@ const fetchCities = (country) => {
});
};
+
+/**
+* Endpoint chill_main_postalcodeapi_search
+* method GET, get Cities Object
+* @params {string} search a search string
+* @params {object} country a country object
+* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string
+*/
+const searchCities = (search, country) => {
+ const url = `/api/1.0/main/postal-code/search.json?q=${search}&country=${country.id}`;
+ return fetch(url)
+ .then(response => {
+ if (response.ok) { return response.json(); }
+ throw Error('Error with request resource response');
+ });
+};
+
+/**
+* Endpoint chill_main_addressreferenceapi_search
+* method GET, get AddressReference Object
+* @params {string} search a search string
+* @params {object} postalCode a postalCode object
+* @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string
+*/
+const searchReferenceAddresses = (search, postalCode) => {
+ const url = `/api/1.0/main/address-reference/by-postal-code/${postalCode.id}/search.json?q=${search}`;
+ return fetch(url)
+ .then(response => {
+ if (response.ok) { return response.json(); }
+ throw Error('Error with request resource response');
+ });
+};
+
+
/**
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
@@ -170,5 +205,7 @@ export {
postAddress,
patchAddress,
postPostalCode,
- getAddress
+ getAddress,
+ searchCities,
+ searchReferenceAddresses
};
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
index 87ecff7fb..ed1d14633 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue
@@ -556,8 +556,8 @@ export default {
this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: null;
this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: null;
- this.entity.selected.writeNew.address = this.context.edit;
- this.entity.selected.writeNew.postcode = this.context.edit;
+ this.entity.selected.writeNew.address = this.context.edit && this.entity.address.addressReference === null && this.entity.address.street.length > 0
+ this.entity.selected.writeNew.postcode = false // NB: this used to be this.context.edit, but think it was erroneous;
console.log('!! just set writeNew.postcode to', this.entity.selected.writeNew.postcode);
},
@@ -569,7 +569,6 @@ export default {
applyChanges()
{
console.log('apply changes');
-
let newAddress = {
'isNoAddress': this.entity.selected.isNoAddress,
'street': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.street,
@@ -633,7 +632,6 @@ export default {
if (!this.context.edit) {
this.addNewAddress(newAddress)
.then(payload => this.addressChangedCallback(payload));
-
} else {
this.updateAddress({
addressId: this.context.addressId,
@@ -697,8 +695,7 @@ export default {
* Async PATCH transactions,
* then update existing address with backend datas when promise is resolved
*/
- updateAddress(payload)
- {
+ updateAddress(payload) {
this.flag.loading = true;
// TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode
diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
index 2409dca53..c333961db 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
+++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress/AddressSelection.vue
@@ -18,6 +18,7 @@
:taggable="true"
:multiple="false"
@tag="addAddress"
+ :loading="isLoading"
:options="addresses">
@@ -48,14 +49,17 @@
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js
index c2ee09960..8047aea11 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/i18n.js
@@ -24,6 +24,7 @@ const visMessages = {
refresh: "Rafraîchir",
screenshot: "Prendre une photo",
choose_relation: "Choisissez le lien de parenté",
+ relationship_household: "Filiation du ménage",
},
edit: 'Éditer',
del: 'Supprimer',
diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js
index ca76f283b..5e8989e49 100644
--- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js
+++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/VisGraph/index.js
@@ -16,7 +16,12 @@ persons.forEach(person => {
})
const app = createApp({
- template: `