mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 03:08:25 +00:00 
			
		
		
		
	Merge remote-tracking branch 'origin/master' into issue279_accompanying_period_validation
This commit is contained in:
		
							
								
								
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -12,8 +12,14 @@ and this project adheres to
 | 
			
		||||
 | 
			
		||||
<!-- write down unreleased development here -->
 | 
			
		||||
* [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
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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")
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,77 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\ActivityBundle\EntityListener;
 | 
			
		||||
 | 
			
		||||
use Chill\ActivityBundle\Entity\Activity;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
 | 
			
		||||
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
 | 
			
		||||
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
 | 
			
		||||
use DateTimeImmutable;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
 | 
			
		||||
use function in_array;
 | 
			
		||||
 | 
			
		||||
class ActivityEntityListener
 | 
			
		||||
{
 | 
			
		||||
    private EntityManagerInterface $em;
 | 
			
		||||
 | 
			
		||||
    private AccompanyingPeriodWorkRepository $workRepository;
 | 
			
		||||
 | 
			
		||||
    public function __construct(EntityManagerInterface $em, AccompanyingPeriodWorkRepository $workRepository)
 | 
			
		||||
    {
 | 
			
		||||
        $this->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();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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]);
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
@@ -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')) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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')) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
     *
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
         :taggable="true"
 | 
			
		||||
         :multiple="false"
 | 
			
		||||
         @tag="addAddress"
 | 
			
		||||
         :loading="isLoading"
 | 
			
		||||
         :options="addresses">
 | 
			
		||||
      </VueMultiselect>
 | 
			
		||||
   </div>
 | 
			
		||||
@@ -48,14 +49,17 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import VueMultiselect from 'vue-multiselect';
 | 
			
		||||
import { searchReferenceAddresses, fetchReferenceAddresses } from '../../api.js';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
   name: 'AddressSelection',
 | 
			
		||||
   components: { VueMultiselect },
 | 
			
		||||
   props: ['entity', 'updateMapCenter'],
 | 
			
		||||
   props: ['entity', 'context', 'updateMapCenter'],
 | 
			
		||||
   data() {
 | 
			
		||||
      return {
 | 
			
		||||
         value: null
 | 
			
		||||
         value: this.context.edit ? this.entity.address.addressReference : null,
 | 
			
		||||
         isLoading: false
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   computed: {
 | 
			
		||||
@@ -107,6 +111,36 @@ export default {
 | 
			
		||||
      },
 | 
			
		||||
      listenInputSearch(query) {
 | 
			
		||||
         //console.log('listenInputSearch', query, this.isAddressSelectorOpen);
 | 
			
		||||
         if (!this.entity.selected.writeNew.postcode) {
 | 
			
		||||
            if (query.length > 2) {
 | 
			
		||||
               this.isLoading = true;
 | 
			
		||||
               searchReferenceAddresses(query, this.entity.selected.city).then(
 | 
			
		||||
                  addresses => new Promise((resolve, reject) => {
 | 
			
		||||
                     this.entity.loaded.addresses = addresses.results;
 | 
			
		||||
                     this.isLoading = false;
 | 
			
		||||
                     resolve();
 | 
			
		||||
               }))
 | 
			
		||||
               .catch((error) => {
 | 
			
		||||
                  console.log(error); //TODO better error handling
 | 
			
		||||
                  this.isLoading = false;
 | 
			
		||||
               });
 | 
			
		||||
            } else {
 | 
			
		||||
               if (query.length === 0) { // Fetch all cities when suppressing the query
 | 
			
		||||
                  this.isLoading = true;
 | 
			
		||||
                  fetchReferenceAddresses(this.entity.selected.city).then(
 | 
			
		||||
                     addresses => new Promise((resolve, reject) => {
 | 
			
		||||
                        this.entity.loaded.addresses = addresses.results;
 | 
			
		||||
                        this.isLoading = false;
 | 
			
		||||
                        resolve();
 | 
			
		||||
                  }))
 | 
			
		||||
                  .catch((error) => {
 | 
			
		||||
                     console.log(error);
 | 
			
		||||
                     this.isLoading = false;
 | 
			
		||||
                  });
 | 
			
		||||
               }
 | 
			
		||||
            }
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         if (this.isAddressSelectorOpen) {
 | 
			
		||||
            this.$data.value = { text: query };
 | 
			
		||||
         } else if (this.isEnteredCustomAddress) {
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@
 | 
			
		||||
         :multiple="false"
 | 
			
		||||
         @tag="addPostcode"
 | 
			
		||||
         :tagPlaceholder="$t('create_postal_code')"
 | 
			
		||||
         :loading="isLoading"
 | 
			
		||||
         :options="cities">
 | 
			
		||||
      </VueMultiselect>
 | 
			
		||||
   </div>
 | 
			
		||||
@@ -48,15 +49,17 @@
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
import VueMultiselect from 'vue-multiselect';
 | 
			
		||||
import { searchCities, fetchCities } from '../../api.js';
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
   name: 'CitySelection',
 | 
			
		||||
   components: { VueMultiselect },
 | 
			
		||||
   props: ['entity', 'focusOnAddress', 'updateMapCenter'],
 | 
			
		||||
   props: ['entity', 'context', 'focusOnAddress', 'updateMapCenter'],
 | 
			
		||||
   emits: ['getReferenceAddresses'],
 | 
			
		||||
   data() {
 | 
			
		||||
      return {
 | 
			
		||||
         value: null
 | 
			
		||||
         value: this.context.edit ? this.entity.address.postcode : null,
 | 
			
		||||
         isLoading: false
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   computed: {
 | 
			
		||||
@@ -93,6 +96,15 @@ export default {
 | 
			
		||||
   },
 | 
			
		||||
   mounted() {
 | 
			
		||||
      console.log('writeNew.postcode', this.entity.selected.writeNew.postcode, 'in mounted');
 | 
			
		||||
      if (this.context.edit) {
 | 
			
		||||
         this.entity.selected.city = this.value;
 | 
			
		||||
         this.entity.selected.postcode.name = this.value.name;
 | 
			
		||||
         this.entity.selected.postcode.code = this.value.code;
 | 
			
		||||
         this.$emit('getReferenceAddresses', this.value);
 | 
			
		||||
         if (this.value.center) {
 | 
			
		||||
            this.updateMapCenter(this.value.center);
 | 
			
		||||
         }
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   methods: {
 | 
			
		||||
      transName(value) {
 | 
			
		||||
@@ -105,7 +117,6 @@ export default {
 | 
			
		||||
         this.entity.selected.postcode.code = value.code;
 | 
			
		||||
         this.entity.selected.postcode.coordinates = value.center.coordinates;
 | 
			
		||||
         this.entity.selected.writeNew.postcode = false;
 | 
			
		||||
         console.log('writeNew.postcode false, in selectCity');
 | 
			
		||||
         this.$emit('getReferenceAddresses', value);
 | 
			
		||||
         this.focusOnAddress();
 | 
			
		||||
         if (value.center) {
 | 
			
		||||
@@ -113,7 +124,33 @@ export default {
 | 
			
		||||
         }
 | 
			
		||||
      },
 | 
			
		||||
      listenInputSearch(query) {
 | 
			
		||||
         //console.log('listenInputSearch', query, this.isCitySelectorOpen);
 | 
			
		||||
         if (query.length > 2) {
 | 
			
		||||
            this.isLoading = true;
 | 
			
		||||
            searchCities(query, this.entity.selected.country).then(
 | 
			
		||||
               cities => new Promise((resolve, reject) => {
 | 
			
		||||
                  this.entity.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
 | 
			
		||||
                  this.isLoading = false;
 | 
			
		||||
                  resolve();
 | 
			
		||||
            }))
 | 
			
		||||
            .catch((error) => {
 | 
			
		||||
               console.log(error); //TODO better error handling
 | 
			
		||||
               this.isLoading = false;
 | 
			
		||||
            });
 | 
			
		||||
         } else {
 | 
			
		||||
            if (query.length === 0) { // Fetch all cities when suppressing the query
 | 
			
		||||
               this.isLoading = true;
 | 
			
		||||
               fetchCities(this.entity.selected.country).then(
 | 
			
		||||
                  cities => new Promise((resolve, reject) => {
 | 
			
		||||
                     this.entity.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
 | 
			
		||||
                     this.isLoading = false;
 | 
			
		||||
                     resolve();
 | 
			
		||||
               }))
 | 
			
		||||
               .catch((error) => {
 | 
			
		||||
                  console.log(error)
 | 
			
		||||
                  this.isLoading = false;
 | 
			
		||||
               });
 | 
			
		||||
            }
 | 
			
		||||
         }
 | 
			
		||||
         if (this.isCitySelectorOpen) {
 | 
			
		||||
            this.$data.value = { text: query };
 | 
			
		||||
         } else if (this.isEnteredCustomCity) {
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,7 @@
 | 
			
		||||
 | 
			
		||||
            <city-selection
 | 
			
		||||
               v-bind:entity="entity"
 | 
			
		||||
               v-bind:context="context"
 | 
			
		||||
               v-bind:focusOnAddress="focusOnAddress"
 | 
			
		||||
               v-bind:updateMapCenter="updateMapCenter"
 | 
			
		||||
               @getReferenceAddresses="$emit('getReferenceAddresses', selected.city)">
 | 
			
		||||
@@ -37,6 +38,7 @@
 | 
			
		||||
 | 
			
		||||
            <address-selection v-if="!isNoAddress"
 | 
			
		||||
               v-bind:entity="entity"
 | 
			
		||||
               v-bind:context="context"
 | 
			
		||||
               v-bind:updateMapCenter="updateMapCenter">
 | 
			
		||||
            </address-selection>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,67 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers.
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\PostalCode;
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class AddressReferenceApiControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    public function provideData()
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        /** @var EntityManagerInterface $em */
 | 
			
		||||
        $em = self::$container->get(EntityManagerInterface::class);
 | 
			
		||||
 | 
			
		||||
        $postalCode = $em->createQueryBuilder()
 | 
			
		||||
            ->select('pc')
 | 
			
		||||
            ->from(PostalCode::class, 'pc')
 | 
			
		||||
            ->where('pc.origin = :origin')
 | 
			
		||||
            ->setParameter('origin', 0)
 | 
			
		||||
            ->setMaxResults(1)
 | 
			
		||||
            ->getQuery()
 | 
			
		||||
            ->getSingleResult();
 | 
			
		||||
 | 
			
		||||
        yield [$postalCode->getId(), 'rue'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider provideData
 | 
			
		||||
     */
 | 
			
		||||
    public function testSearch(int $postCodeId, string $pattern)
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            "/api/1.0/main/address-reference/by-postal-code/{$postCodeId}/search.json",
 | 
			
		||||
            ['q' => $pattern]
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertResponseIsSuccessful();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,57 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers.
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Test\PrepareClientTrait;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
 | 
			
		||||
use function json_decode;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class PostalCodeApiControllerTest extends WebTestCase
 | 
			
		||||
{
 | 
			
		||||
    use PrepareClientTrait;
 | 
			
		||||
 | 
			
		||||
    public function testSearch()
 | 
			
		||||
    {
 | 
			
		||||
        $client = $this->getClientAuthenticated();
 | 
			
		||||
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/api/1.0/main/postal-code/search.json',
 | 
			
		||||
            ['q' => 'fontenay le comte']
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $this->assertResponseIsSuccessful();
 | 
			
		||||
 | 
			
		||||
        $data = json_decode($client->getResponse()->getContent(), true);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals('Fontenay Le Comte', $data['results'][0]['name']);
 | 
			
		||||
 | 
			
		||||
        // test response with invalid search pattern
 | 
			
		||||
        $client->request(
 | 
			
		||||
            'GET',
 | 
			
		||||
            '/api/1.0/main/postal-code/search.json',
 | 
			
		||||
            ['q' => '']
 | 
			
		||||
        );
 | 
			
		||||
        $this->assertResponseStatusCodeSame(400);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -204,6 +204,14 @@ final class PaginatorTest extends KernelTestCase
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testPagesWithoutResult()
 | 
			
		||||
    {
 | 
			
		||||
        $paginator = $this->generatePaginator(0, 10);
 | 
			
		||||
 | 
			
		||||
        $this->assertEquals(0, $paginator->getCurrentPageFirstItemNumber());
 | 
			
		||||
        $this->assertEquals(10, $paginator->getItemsPerPage());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param int $itemPerPage
 | 
			
		||||
     * @param string $route
 | 
			
		||||
 
 | 
			
		||||
@@ -360,6 +360,40 @@ paths:
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
 | 
			
		||||
    /1.0/main/address-reference/by-postal-code/{id}/search.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - address
 | 
			
		||||
                - search
 | 
			
		||||
            summary: Return a reference address by id
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: id
 | 
			
		||||
                    in: path
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The reference address id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
                        format: integer
 | 
			
		||||
                        minimum: 1
 | 
			
		||||
                -   name: q
 | 
			
		||||
                    in: query
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The search pattern
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                    content:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                $ref: '#/components/schemas/AddressReference'
 | 
			
		||||
                404:
 | 
			
		||||
                    description: "not found"
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
                400:
 | 
			
		||||
                    description: "Bad request"
 | 
			
		||||
    /1.0/main/postal-code.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
@@ -430,6 +464,37 @@ paths:
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
 | 
			
		||||
    /1.0/main/postal-code/search.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - address
 | 
			
		||||
                - search
 | 
			
		||||
            summary: Search a postal code
 | 
			
		||||
            parameters:
 | 
			
		||||
                -   name: q
 | 
			
		||||
                    in: query
 | 
			
		||||
                    required: true
 | 
			
		||||
                    description: The search pattern
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: string
 | 
			
		||||
                -   name: country
 | 
			
		||||
                    in: query
 | 
			
		||||
                    required: false
 | 
			
		||||
                    description: The country id
 | 
			
		||||
                    schema:
 | 
			
		||||
                        type: integer
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                    content:
 | 
			
		||||
                        application/json:
 | 
			
		||||
                            schema:
 | 
			
		||||
                                $ref: '#/components/schemas/PostalCode'
 | 
			
		||||
                404:
 | 
			
		||||
                    description: "not found"
 | 
			
		||||
                400:
 | 
			
		||||
                    description: "Bad Request"
 | 
			
		||||
 | 
			
		||||
    /1.0/main/country.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20211125142016 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP TRIGGER canonicalize_address_reference_on_insert ON chill_main_address_reference');
 | 
			
		||||
        $this->addSql('DROP TRIGGER canonicalize_address_reference_on_update ON chill_main_address_reference');
 | 
			
		||||
        $this->addSql('DROP FUNCTION canonicalize_address_reference()');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_address_reference DROP COLUMN addressCanonical');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add a column "canonicalized" on chill_main_address_reference and add trigger and indexed on it';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_address_reference ADD addressCanonical TEXT DEFAULT \'\' NOT NULL');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('UPDATE chill_main_address_reference
 | 
			
		||||
        SET addresscanonical =
 | 
			
		||||
                TRIM(
 | 
			
		||||
                    UNACCENT(
 | 
			
		||||
                        LOWER(
 | 
			
		||||
                            street ||
 | 
			
		||||
                            \' \' ||
 | 
			
		||||
                            streetnumber
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                )');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE OR REPLACE FUNCTION public.canonicalize_address_reference() RETURNS TRIGGER
 | 
			
		||||
            LANGUAGE plpgsql
 | 
			
		||||
        AS
 | 
			
		||||
        $$
 | 
			
		||||
        BEGIN
 | 
			
		||||
            NEW.addresscanonical =
 | 
			
		||||
                TRIM(
 | 
			
		||||
                    UNACCENT(
 | 
			
		||||
                        LOWER(
 | 
			
		||||
                            NEW.street ||
 | 
			
		||||
                            \' \' ||
 | 
			
		||||
                            NEW.streetnumber
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                )
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
            return NEW;
 | 
			
		||||
        END
 | 
			
		||||
        $$');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE TRIGGER canonicalize_address_reference_on_insert
 | 
			
		||||
            BEFORE INSERT
 | 
			
		||||
            ON chill_main_address_reference
 | 
			
		||||
            FOR EACH ROW
 | 
			
		||||
                EXECUTE procedure canonicalize_address_reference()');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE TRIGGER canonicalize_address_reference_on_update
 | 
			
		||||
            BEFORE UPDATE
 | 
			
		||||
            ON chill_main_address_reference
 | 
			
		||||
            FOR EACH ROW
 | 
			
		||||
                EXECUTE procedure canonicalize_address_reference()');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE INDEX chill_internal_address_reference_canonicalized ON chill_main_address_reference USING GIST (postcode_id, addressCanonical gist_trgm_ops)');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,85 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chill is a software for social workers
 | 
			
		||||
 *
 | 
			
		||||
 * For the full copyright and license information, please view
 | 
			
		||||
 * the LICENSE file that was distributed with this source code.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare(strict_types=1);
 | 
			
		||||
 | 
			
		||||
namespace Chill\Migrations\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20211125142017 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP TRIGGER canonicalize_postal_code_on_insert ON chill_main_postal_code');
 | 
			
		||||
        $this->addSql('DROP TRIGGER canonicalize_postal_code_on_update ON chill_main_postal_code');
 | 
			
		||||
        $this->addSql('DROP FUNCTION canonicalize_postal_code()');
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_postal_code DROP COLUMN canonical');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Add a column "canonicalized" on postal code';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('ALTER TABLE chill_main_postal_code ADD canonical TEXT DEFAULT \'\' NOT NULL');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('UPDATE chill_main_postal_code
 | 
			
		||||
        SET canonical =
 | 
			
		||||
                TRIM(
 | 
			
		||||
                    UNACCENT(
 | 
			
		||||
                        LOWER(
 | 
			
		||||
                            code ||
 | 
			
		||||
                            \' \' ||
 | 
			
		||||
                            label
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                )');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE OR REPLACE FUNCTION public.canonicalize_postal_code() RETURNS TRIGGER
 | 
			
		||||
            LANGUAGE plpgsql
 | 
			
		||||
        AS
 | 
			
		||||
        $$
 | 
			
		||||
        BEGIN
 | 
			
		||||
            NEW.canonical =
 | 
			
		||||
                TRIM(
 | 
			
		||||
                    UNACCENT(
 | 
			
		||||
                        LOWER(
 | 
			
		||||
                            NEW.code ||
 | 
			
		||||
                            \' \' ||
 | 
			
		||||
                            NEW.label
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
                )
 | 
			
		||||
            ;
 | 
			
		||||
 | 
			
		||||
            return NEW;
 | 
			
		||||
        END
 | 
			
		||||
        $$');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE TRIGGER canonicalize_postal_code_on_insert
 | 
			
		||||
            BEFORE INSERT
 | 
			
		||||
            ON chill_main_postal_code
 | 
			
		||||
            FOR EACH ROW
 | 
			
		||||
                EXECUTE procedure canonicalize_postal_code()');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE TRIGGER canonicalize_postal_code_on_update
 | 
			
		||||
            BEFORE UPDATE
 | 
			
		||||
            ON chill_main_postal_code
 | 
			
		||||
            FOR EACH ROW
 | 
			
		||||
                EXECUTE procedure canonicalize_postal_code()');
 | 
			
		||||
 | 
			
		||||
        $this->addSql('CREATE INDEX chill_internal_postal_code_canonicalized ON chill_main_postal_code USING GIST (canonical gist_trgm_ops) WHERE origin = 0');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,7 +14,6 @@ namespace Chill\PersonBundle\Form;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillDateType;
 | 
			
		||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
 | 
			
		||||
use Symfony\Component\Form\AbstractType;
 | 
			
		||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
 | 
			
		||||
use Symfony\Component\Form\FormBuilderInterface;
 | 
			
		||||
 | 
			
		||||
class HouseholdMemberType extends AbstractType
 | 
			
		||||
@@ -26,17 +25,6 @@ class HouseholdMemberType extends AbstractType
 | 
			
		||||
                'label' => 'household.Start date',
 | 
			
		||||
                'input' => 'datetime_immutable',
 | 
			
		||||
            ]);
 | 
			
		||||
 | 
			
		||||
        if ($options['data']->getPosition()->isAllowHolder()) {
 | 
			
		||||
            $builder
 | 
			
		||||
                ->add('holder', ChoiceType::class, [
 | 
			
		||||
                    'label' => 'household.holder',
 | 
			
		||||
                    'choices' => [
 | 
			
		||||
                        'household.is holder' => true,
 | 
			
		||||
                        'household.is not holder' => false,
 | 
			
		||||
                    ],
 | 
			
		||||
                ]);
 | 
			
		||||
        }
 | 
			
		||||
        $builder
 | 
			
		||||
            ->add('comment', ChillTextareaType::class, [
 | 
			
		||||
                'label' => 'household.Comment',
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@
 | 
			
		||||
                  <div class="row">
 | 
			
		||||
                     <div class="col-12 text-center">{{ $t('visgraph.between') }}<br>{{ $t('visgraph.and') }}</div>
 | 
			
		||||
                     <div class="col">
 | 
			
		||||
                        <small>{{ getPersonAge(modal.data.from) }}</small>
 | 
			
		||||
                        <h4>{{ getPerson(modal.data.from).text }}</h4>
 | 
			
		||||
                        <p class="text-start" v-if="relation && relation.title">
 | 
			
		||||
                           <span v-if="reverse">
 | 
			
		||||
@@ -64,6 +65,7 @@
 | 
			
		||||
                        </p>
 | 
			
		||||
                     </div>
 | 
			
		||||
                     <div class="col text-end">
 | 
			
		||||
                        <small>{{ getPersonAge(modal.data.to) }}</small>
 | 
			
		||||
                        <h4>{{ getPerson(modal.data.to).text }}</h4>
 | 
			
		||||
                        <p class="text-end" v-if="relation && relation.title">
 | 
			
		||||
                           <span v-if="reverse">
 | 
			
		||||
@@ -119,8 +121,9 @@ import vis from 'vis-network/dist/vis-network'
 | 
			
		||||
import { mapState, mapGetters } from "vuex"
 | 
			
		||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal'
 | 
			
		||||
import VueMultiselect from 'vue-multiselect'
 | 
			
		||||
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api";
 | 
			
		||||
import { splitId } from "./vis-network";
 | 
			
		||||
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api"
 | 
			
		||||
import { splitId, getAge } from "./vis-network"
 | 
			
		||||
import { visMessages } from "./i18n";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
   name: "App",
 | 
			
		||||
@@ -128,6 +131,7 @@ export default {
 | 
			
		||||
      Modal,
 | 
			
		||||
      VueMultiselect
 | 
			
		||||
   },
 | 
			
		||||
   props: ['household_id'],
 | 
			
		||||
   data() {
 | 
			
		||||
      return {
 | 
			
		||||
         container: '',
 | 
			
		||||
@@ -152,7 +156,9 @@ export default {
 | 
			
		||||
               class: null,
 | 
			
		||||
               text: null
 | 
			
		||||
            },
 | 
			
		||||
         }
 | 
			
		||||
         },
 | 
			
		||||
         canvas: null,
 | 
			
		||||
         link: null,
 | 
			
		||||
      }
 | 
			
		||||
   },
 | 
			
		||||
   computed: {
 | 
			
		||||
@@ -164,7 +170,7 @@ export default {
 | 
			
		||||
      ]),
 | 
			
		||||
 | 
			
		||||
      visgraph_data() {
 | 
			
		||||
         console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges')
 | 
			
		||||
         //console.log('::: visgraph_data :::', this.nodes.length, 'nodes,', this.edges.length, 'edges')
 | 
			
		||||
         return {
 | 
			
		||||
            nodes: this.nodes,
 | 
			
		||||
            edges: this.edges
 | 
			
		||||
@@ -172,12 +178,12 @@ export default {
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      refreshNetwork() {
 | 
			
		||||
         console.log('--- refresh network')
 | 
			
		||||
         //console.log('--- refresh network')
 | 
			
		||||
         window.network.setData(this.visgraph_data)
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      legendLayers() {
 | 
			
		||||
         console.log('--- refresh legend and rebuild checked Layers')
 | 
			
		||||
         //console.log('--- refresh legend and rebuild checked Layers')
 | 
			
		||||
         this.checkedLayers = []
 | 
			
		||||
         let layersDisplayed = [
 | 
			
		||||
            ...this.nodes.filter(n => n.id.startsWith('household')),
 | 
			
		||||
@@ -193,7 +199,7 @@ export default {
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      checkedLayers() { // required to refresh data checkedLayers
 | 
			
		||||
         console.log('--- checkedLayers')
 | 
			
		||||
         //console.log('--- checkedLayers')
 | 
			
		||||
         return this.checkedLayers
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
@@ -218,7 +224,7 @@ export default {
 | 
			
		||||
   },
 | 
			
		||||
   watch: {
 | 
			
		||||
      updateHack(newValue, oldValue) {
 | 
			
		||||
         console.log(`--- updateHack ${oldValue} <> ${newValue}`)
 | 
			
		||||
         //console.log(`--- updateHack ${oldValue} <> ${newValue}`)
 | 
			
		||||
         if (oldValue !== newValue) {
 | 
			
		||||
            this.forceUpdateComponent()
 | 
			
		||||
         }
 | 
			
		||||
@@ -229,6 +235,9 @@ export default {
 | 
			
		||||
      this.initGraph()
 | 
			
		||||
      this.listenOnGraph()
 | 
			
		||||
      this.getRelationsList()
 | 
			
		||||
 | 
			
		||||
      this.canvas = document.getElementById('visgraph').querySelector('canvas')
 | 
			
		||||
      this.link = document.getElementById('exportCanvasBtn')
 | 
			
		||||
   },
 | 
			
		||||
   methods: {
 | 
			
		||||
 | 
			
		||||
@@ -255,27 +264,27 @@ export default {
 | 
			
		||||
 | 
			
		||||
               case 'person':
 | 
			
		||||
                  let person = this.nodes.filter(n => n.id === node)[0]
 | 
			
		||||
                  console.log('@@@@@@ event on selected Node', person.id)
 | 
			
		||||
                  //console.log('@@@@@@ event on selected Node', person.id)
 | 
			
		||||
                  if (this.listenPersonFlag === 'normal') {
 | 
			
		||||
                     if (person.folded === true) {
 | 
			
		||||
                        console.log('   @@> expand mode event')
 | 
			
		||||
                        //console.log('   @@> expand mode event')
 | 
			
		||||
                        this.$store.commit('unfoldPerson', person)
 | 
			
		||||
                     }
 | 
			
		||||
                  } else {
 | 
			
		||||
                     console.log('   @@> create link mode event')
 | 
			
		||||
                     //console.log('   @@> create link mode event')
 | 
			
		||||
                     this.listenStepsToAddRelationship(person)
 | 
			
		||||
                  }
 | 
			
		||||
                  break
 | 
			
		||||
 | 
			
		||||
               case 'household':
 | 
			
		||||
                  let household = this.nodes.filter(n => n.id === node)[0]
 | 
			
		||||
                  console.log('@@@@@@ event on selected Node', household.id)
 | 
			
		||||
                  //console.log('@@@@@@ event on selected Node', household.id)
 | 
			
		||||
                  this.$store.dispatch('unfoldPersonsByHousehold', household)
 | 
			
		||||
                  break
 | 
			
		||||
 | 
			
		||||
               case 'accompanying_period':
 | 
			
		||||
                  let course = this.nodes.filter(n => n.id === node)[0]
 | 
			
		||||
                  console.log('@@@@@@ event on selected Node', course.id)
 | 
			
		||||
                  //console.log('@@@@@@ event on selected Node', course.id)
 | 
			
		||||
                  this.$store.dispatch('unfoldPersonsByCourse', course)
 | 
			
		||||
                  break
 | 
			
		||||
 | 
			
		||||
@@ -290,7 +299,7 @@ export default {
 | 
			
		||||
            }
 | 
			
		||||
            let link = data.edges[0]
 | 
			
		||||
            let linkType = splitId(link, 'link')
 | 
			
		||||
            console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data)
 | 
			
		||||
            //console.log('@@@@@ event on selected Edge', data.edges.length, linkType, data)
 | 
			
		||||
 | 
			
		||||
            if (linkType.startsWith('relationship')) {
 | 
			
		||||
               //console.log('linkType relationship')
 | 
			
		||||
@@ -314,7 +323,7 @@ export default {
 | 
			
		||||
         })
 | 
			
		||||
      },
 | 
			
		||||
      listenStepsToAddRelationship(person) {
 | 
			
		||||
         console.log('   @@> listenStep', this.listenPersonFlag)
 | 
			
		||||
         //console.log('   @@> listenStep', this.listenPersonFlag)
 | 
			
		||||
         if (this.listenPersonFlag === 'step2') {
 | 
			
		||||
            //console.log('   @@> person 2', person)
 | 
			
		||||
            this.newEdgeData.to = person.id
 | 
			
		||||
@@ -333,7 +342,7 @@ export default {
 | 
			
		||||
      /// control Layers
 | 
			
		||||
      toggleLayer(value) {
 | 
			
		||||
         let id = value.target.value
 | 
			
		||||
         console.log('@@@@@@ toggle Layer', id)
 | 
			
		||||
         //console.log('@@@@@@ toggle Layer', id)
 | 
			
		||||
         this.forceUpdateComponent()
 | 
			
		||||
         if (this.checkedLayers.includes(id)) {
 | 
			
		||||
            this.removeLayer(id)
 | 
			
		||||
@@ -382,7 +391,7 @@ export default {
 | 
			
		||||
            title: null,
 | 
			
		||||
            button: { class: null, text: null, }
 | 
			
		||||
         }
 | 
			
		||||
         console.log('==- reset Form', this.modal.data)
 | 
			
		||||
         //console.log('==- reset Form', this.modal.data)
 | 
			
		||||
      },
 | 
			
		||||
      getRelationsList() {
 | 
			
		||||
         //console.log('fetch relationsList')
 | 
			
		||||
@@ -400,12 +409,16 @@ export default {
 | 
			
		||||
         let person = this.persons.filter(p => p.id === id)
 | 
			
		||||
         return person[0]
 | 
			
		||||
      },
 | 
			
		||||
      getPersonAge(id) {
 | 
			
		||||
         let person = this.getPerson(id)
 | 
			
		||||
         return getAge(person)
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // actions
 | 
			
		||||
      createRelationship() {
 | 
			
		||||
         this.displayHelpMessage = true
 | 
			
		||||
         this.listenPersonFlag = 'step1' // toggle listener in create link mode
 | 
			
		||||
         console.log('   @@> switch listener to create link mode:', this.listenPersonFlag)
 | 
			
		||||
         //console.log('   @@> switch listener to create link mode:', this.listenPersonFlag)
 | 
			
		||||
      },
 | 
			
		||||
      dropRelationship() {
 | 
			
		||||
         //console.log('delete', this.modal.data)
 | 
			
		||||
@@ -417,13 +430,13 @@ export default {
 | 
			
		||||
         this.forceUpdateComponent()
 | 
			
		||||
      },
 | 
			
		||||
      submitRelationship() {
 | 
			
		||||
         console.log('submitRelationship', this.modal.action)
 | 
			
		||||
         //console.log('submitRelationship', this.modal.action)
 | 
			
		||||
         switch (this.modal.action) {
 | 
			
		||||
 | 
			
		||||
            case 'create':
 | 
			
		||||
               return postRelationship(this.modal.data)
 | 
			
		||||
               .then(relationship => new Promise(resolve => {
 | 
			
		||||
                  console.log('post relationship response', relationship)
 | 
			
		||||
                  //console.log('post relationship response', relationship)
 | 
			
		||||
                  this.$store.dispatch('addLinkFromRelationship', relationship)
 | 
			
		||||
                  this.modal.showModal = false
 | 
			
		||||
                  this.resetForm()
 | 
			
		||||
@@ -435,7 +448,7 @@ export default {
 | 
			
		||||
            case 'edit':
 | 
			
		||||
               return patchRelationship(this.modal.data)
 | 
			
		||||
               .then(relationship => new Promise(resolve => {
 | 
			
		||||
                  console.log('patch relationship response', relationship)
 | 
			
		||||
                  //console.log('patch relationship response', relationship)
 | 
			
		||||
                  this.$store.commit('updateLink', relationship)
 | 
			
		||||
                  this.modal.showModal = false
 | 
			
		||||
                  this.resetForm()
 | 
			
		||||
@@ -450,39 +463,44 @@ export default {
 | 
			
		||||
      },
 | 
			
		||||
 | 
			
		||||
      // export image
 | 
			
		||||
      exportCanvasAsImage() {
 | 
			
		||||
         const canvas = document.getElementById('visgraph')
 | 
			
		||||
            .querySelector('canvas')
 | 
			
		||||
         console.log(canvas)
 | 
			
		||||
      async exportCanvasAsImage() {
 | 
			
		||||
 | 
			
		||||
         let link = document.getElementById('exportCanvasBtn')
 | 
			
		||||
         link.download = "filiation.png"
 | 
			
		||||
         let
 | 
			
		||||
            filename = `filiation_${this.household_id}.jpg`,
 | 
			
		||||
            mime = 'image/jpeg',
 | 
			
		||||
            quality = 0.85,
 | 
			
		||||
            footer = `© Chill ${new Date().getFullYear()}`,
 | 
			
		||||
            timestamp = `${visMessages.fr.visgraph.relationship_household} n° ${this.household_id} — ${new Date().toLocaleString()}`
 | 
			
		||||
 | 
			
		||||
         canvas.toBlob(blob => {
 | 
			
		||||
            console.log(blob)
 | 
			
		||||
            link.href = URL.createObjectURL(blob)
 | 
			
		||||
         }, 'image/png')
 | 
			
		||||
         // resolve toBlob in a Promise
 | 
			
		||||
         const getCanvasBlob = canvas => new Promise(resolve => {
 | 
			
		||||
            canvas.toBlob(blob => resolve(blob), mime, quality)
 | 
			
		||||
         })
 | 
			
		||||
 | 
			
		||||
         /*
 | 
			
		||||
         TODO improve feature
 | 
			
		||||
 | 
			
		||||
         // 1. fonctionne, mais pas de contrôle sur le nom
 | 
			
		||||
         if (canvas && canvas.getContext('2d')) {
 | 
			
		||||
            let img = canvas.toDataURL('image/png;base64;')
 | 
			
		||||
            img = img.replace('image/png','image/octet-stream')
 | 
			
		||||
            window.open(img, '', 'width=1000, height=1000')
 | 
			
		||||
         }
 | 
			
		||||
 | 
			
		||||
         // 2. fonctionne, mais 2 click et pas compatible avec tous les browsers
 | 
			
		||||
         let link = document.getElementById('exportCanvasBtn')
 | 
			
		||||
         link.download = "image.png"
 | 
			
		||||
         canvas.toBlob(blob => {
 | 
			
		||||
            link.href = URL.createObjectURL(blob)
 | 
			
		||||
         }, 'image/png')
 | 
			
		||||
         */
 | 
			
		||||
         // build image from new temporary canvas
 | 
			
		||||
         let tmpCanvas = document.createElement('canvas')
 | 
			
		||||
         tmpCanvas.width = this.canvas.width
 | 
			
		||||
         tmpCanvas.height = this.canvas.height
 | 
			
		||||
 | 
			
		||||
         let ctx = tmpCanvas.getContext('2d')
 | 
			
		||||
         ctx.beginPath()
 | 
			
		||||
         ctx.fillStyle = '#fff'
 | 
			
		||||
         ctx.fillRect(0, 0, tmpCanvas.width, tmpCanvas.height);
 | 
			
		||||
         ctx.fillStyle = '#9d4600'
 | 
			
		||||
         ctx.fillText(footer +' — '+ timestamp, 5, tmpCanvas.height - 10)
 | 
			
		||||
         ctx.drawImage(this.canvas, 0, 0)
 | 
			
		||||
 | 
			
		||||
         return await getCanvasBlob(tmpCanvas)
 | 
			
		||||
            .then(blob => {
 | 
			
		||||
               let url = document.createElement("a")
 | 
			
		||||
               url.download = filename
 | 
			
		||||
               url.href = window.URL.createObjectURL(blob)
 | 
			
		||||
               url.click()
 | 
			
		||||
               console.log('url', url.href)
 | 
			
		||||
               URL.revokeObjectURL(url.href)
 | 
			
		||||
         })
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
   }
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 
 | 
			
		||||
@@ -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',
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,12 @@ persons.forEach(person => {
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const app = createApp({
 | 
			
		||||
    template: `<app></app>`
 | 
			
		||||
    template: `<app :household_id="this.household_id"></app>`,
 | 
			
		||||
    data() {
 | 
			
		||||
        return {
 | 
			
		||||
            household_id: JSON.parse(container.dataset.householdId)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
})
 | 
			
		||||
.use(store)
 | 
			
		||||
.use(i18n)
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ const store = createStore({
 | 
			
		||||
                }
 | 
			
		||||
            })
 | 
			
		||||
            //console.log('array', array.map(item => item.person.id))
 | 
			
		||||
            console.log('get persons group', group.map(f => f.id))
 | 
			
		||||
            //console.log('get persons group', group.map(f => f.id))
 | 
			
		||||
            return group
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
@@ -120,13 +120,17 @@ const store = createStore({
 | 
			
		||||
    },
 | 
			
		||||
    mutations: {
 | 
			
		||||
        addPerson(state, [person, options]) {
 | 
			
		||||
            let age = getAge(person)
 | 
			
		||||
            age = (age === '')? '' : ' - ' + age
 | 
			
		||||
 | 
			
		||||
            let debug = ''
 | 
			
		||||
            /// Debug mode: uncomment to display person_id on visgraph
 | 
			
		||||
            //debug = `\nid ${person.id}`
 | 
			
		||||
 | 
			
		||||
            person.group = person.type
 | 
			
		||||
            person._id = person.id
 | 
			
		||||
            person.id = `person_${person.id}`
 | 
			
		||||
            person.label = `*${person.text}*\n_${getGender(person.gender)} - ${getAge(person.birthdate)}_${debug}` //
 | 
			
		||||
            person.label = `*${person.text}*\n_${getGender(person.gender)}${age}_${debug}`
 | 
			
		||||
            person.folded = false
 | 
			
		||||
            // folded is used for missing persons
 | 
			
		||||
            if (options.folded) {
 | 
			
		||||
@@ -161,7 +165,7 @@ const store = createStore({
 | 
			
		||||
            state.links.push(link)
 | 
			
		||||
        },
 | 
			
		||||
        updateLink(state, link) {
 | 
			
		||||
            console.log('updateLink', link)
 | 
			
		||||
            //console.log('updateLink', link)
 | 
			
		||||
            let link_ = {
 | 
			
		||||
                from: `person_${link.fromPerson.id}`,
 | 
			
		||||
                to: `person_${link.toPerson.id}`,
 | 
			
		||||
@@ -264,7 +268,7 @@ const store = createStore({
 | 
			
		||||
        fetchInfoForPerson({ dispatch }, person) {
 | 
			
		||||
                // TODO enfants hors ménages
 | 
			
		||||
                // example: household 61
 | 
			
		||||
                // console.log(person.text, 'household', person.current_household_id)
 | 
			
		||||
                //console.log(person.text, 'household', person.current_household_id)
 | 
			
		||||
            if (null !== person.current_household_id) {
 | 
			
		||||
                dispatch('fetchHouseholdForPerson', person)
 | 
			
		||||
            }
 | 
			
		||||
@@ -305,15 +309,16 @@ const store = createStore({
 | 
			
		||||
         */
 | 
			
		||||
        addLinkFromPersonsToHousehold({ commit, getters, dispatch }, household) {
 | 
			
		||||
            let members = getters.getMembersByHousehold(household.id)
 | 
			
		||||
            console.log('add link for', members.length, 'members')
 | 
			
		||||
            //console.log('add link for', members.length, 'members')
 | 
			
		||||
            members.forEach(m => {
 | 
			
		||||
                commit('addLink', {
 | 
			
		||||
                    from: `${m.person.type}_${m.person.id}`,
 | 
			
		||||
                    to: `household_${m.person.current_household_id}`,
 | 
			
		||||
                    id: `household_${m.person.current_household_id}-person_${m.person.id}`,
 | 
			
		||||
                    to: `${household.id}`,
 | 
			
		||||
                    id: `${household.id}-person_${m.person.id}`,
 | 
			
		||||
                    arrows: 'from',
 | 
			
		||||
                    color: 'pink',
 | 
			
		||||
                    font: { color: '#D04A60' },
 | 
			
		||||
                    dashes: (getHouseholdWidth(m) === 1)? [0,4] : false,  //edge style:  [dash, gap, dash, gap]
 | 
			
		||||
                    label: getHouseholdLabel(m),
 | 
			
		||||
                    width: getHouseholdWidth(m),
 | 
			
		||||
                })
 | 
			
		||||
@@ -362,7 +367,7 @@ const store = createStore({
 | 
			
		||||
         */
 | 
			
		||||
        addLinkFromPersonsToCourse({ commit, getters, dispatch }, course) {
 | 
			
		||||
            const participations = getters.getParticipationsByCourse(course.id)
 | 
			
		||||
            console.log('add link for', participations.length, 'participations')
 | 
			
		||||
            //console.log('add link for', participations.length, 'participations')
 | 
			
		||||
            participations.forEach(p => {
 | 
			
		||||
                //console.log(p.person.id)
 | 
			
		||||
                commit('addLink', {
 | 
			
		||||
@@ -445,7 +450,7 @@ const store = createStore({
 | 
			
		||||
         *  @param array
 | 
			
		||||
         */
 | 
			
		||||
        addMissingPerson({ commit, getters, dispatch }, [person, parent]) {
 | 
			
		||||
            console.log('! add missing Person', person.id)
 | 
			
		||||
            //console.log('! add missing Person', person.id)
 | 
			
		||||
            commit('markPersonLoaded', person.id)
 | 
			
		||||
            commit('addPerson', [person, { folded: true }])
 | 
			
		||||
            if (getters.isExcludedNode(parent.id)) {
 | 
			
		||||
@@ -467,7 +472,7 @@ const store = createStore({
 | 
			
		||||
            getters.getPersonsGroup(participations)
 | 
			
		||||
            .forEach(person => {
 | 
			
		||||
                if (person.folded === true) {
 | 
			
		||||
                    console.log('-=. unfold and expand person', person.id)
 | 
			
		||||
                    //console.log('-=. unfold and expand person', person.id)
 | 
			
		||||
                    commit('unfoldPerson', person)
 | 
			
		||||
                    dispatch('fetchInfoForPerson', person)
 | 
			
		||||
                }
 | 
			
		||||
@@ -485,7 +490,7 @@ const store = createStore({
 | 
			
		||||
            getters.getPersonsGroup(members)
 | 
			
		||||
            .forEach(person => {
 | 
			
		||||
                if (person.folded === true) {
 | 
			
		||||
                    console.log('-=. unfold and expand person', person.id)
 | 
			
		||||
                    //console.log('-=. unfold and expand person', person.id)
 | 
			
		||||
                    commit('unfoldPerson', person)
 | 
			
		||||
                    dispatch('fetchInfoForPerson', person)
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,13 +13,12 @@ window.options = {
 | 
			
		||||
    locale: 'fr',
 | 
			
		||||
    locales: visMessages,
 | 
			
		||||
    /*
 | 
			
		||||
    */
 | 
			
		||||
    configure: {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        filter: 'nodes,edges',
 | 
			
		||||
        //container: undefined,
 | 
			
		||||
        filter: 'physics',
 | 
			
		||||
        showButton: true
 | 
			
		||||
    },
 | 
			
		||||
    */
 | 
			
		||||
    physics: {
 | 
			
		||||
        enabled: true,
 | 
			
		||||
        barnesHut: {
 | 
			
		||||
@@ -37,8 +36,8 @@ window.options = {
 | 
			
		||||
            centralGravity: 0.01,
 | 
			
		||||
            springLength: 100,
 | 
			
		||||
            springConstant: 0.08,
 | 
			
		||||
            damping: 0.4,
 | 
			
		||||
            avoidOverlap: 0
 | 
			
		||||
            damping: 0.75,
 | 
			
		||||
            avoidOverlap: 0.00
 | 
			
		||||
        },
 | 
			
		||||
        repulsion: {
 | 
			
		||||
            centralGravity: 0.2,
 | 
			
		||||
@@ -159,17 +158,21 @@ const getGender = (gender) => {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * TODO Repeat getAge() in PersonRenderBox.vue
 | 
			
		||||
 * @param birthdate
 | 
			
		||||
 * TODO only one abstract function (-> getAge() is repeated in PersonRenderBox.vue)
 | 
			
		||||
 * @param person
 | 
			
		||||
 * @returns {string|null}
 | 
			
		||||
 */
 | 
			
		||||
const getAge = (birthdate) => {
 | 
			
		||||
    if (null === birthdate) {
 | 
			
		||||
        return null
 | 
			
		||||
const getAge = (person) => {
 | 
			
		||||
    if (person.birthdate) {
 | 
			
		||||
        let birthdate = new Date(person.birthdate.datetime)
 | 
			
		||||
        if (person.deathdate) {
 | 
			
		||||
            let deathdate = new Date(person.deathdate.datetime)
 | 
			
		||||
            return (deathdate.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
 | 
			
		||||
        }
 | 
			
		||||
        let now = new Date()
 | 
			
		||||
        return (now.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
 | 
			
		||||
    }
 | 
			
		||||
    const birthday = new Date(birthdate.datetime)
 | 
			
		||||
    const now = new Date()
 | 
			
		||||
    return (now.getFullYear() - birthday.getFullYear()) + ' '+ visMessages.fr.visgraph.years
 | 
			
		||||
    return ''
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -192,6 +192,7 @@ export default {
 | 
			
		||||
            return `/fr/person/${this.person.id}/general`;
 | 
			
		||||
        },
 | 
			
		||||
        getAge: function() {
 | 
			
		||||
            // TODO only one abstract function
 | 
			
		||||
            if(this.person.birthdate && !this.person.deathdate){
 | 
			
		||||
                const birthday = new Date(this.person.birthdate.datetime)
 | 
			
		||||
                const now = new Date()
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,8 @@
 | 
			
		||||
 | 
			
		||||
    <div id="relationship-graph"
 | 
			
		||||
        style="margin-top: -3rem"
 | 
			
		||||
        data-persons="{{ persons|e('html_attr') }}">
 | 
			
		||||
        data-persons="{{ persons|e('html_attr') }}"
 | 
			
		||||
        data-household-id="{{ household.id|e('html_attr') }}">
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ use Chill\PersonBundle\Entity\Household\Household;
 | 
			
		||||
use Chill\PersonBundle\Entity\Person;
 | 
			
		||||
use Chill\PersonBundle\Repository\PersonRepository;
 | 
			
		||||
use DateTime;
 | 
			
		||||
use DateTimeImmutable;
 | 
			
		||||
use LogicException;
 | 
			
		||||
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
 | 
			
		||||
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
 | 
			
		||||
@@ -114,18 +115,14 @@ class PersonJsonNormalizer implements
 | 
			
		||||
                case 'birthdate':
 | 
			
		||||
                    $object = $this->denormalizer->denormalize($data[$item], DateTime::class, $format, $context);
 | 
			
		||||
 | 
			
		||||
                    if ($object instanceof DateTime) {
 | 
			
		||||
                        $person->setBirthdate($object);
 | 
			
		||||
                    }
 | 
			
		||||
                    $person->setBirthdate($object);
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case 'deathdate':
 | 
			
		||||
                    $object = $this->denormalizer->denormalize($data[$item], DateTime::class, $format, $context);
 | 
			
		||||
                    $object = $this->denormalizer->denormalize($data[$item], DateTimeImmutable::class, $format, $context);
 | 
			
		||||
 | 
			
		||||
                    if ($object instanceof DateTime) {
 | 
			
		||||
                        $person->setDeathdate($object);
 | 
			
		||||
                    }
 | 
			
		||||
                    $person->setDeathdate($object);
 | 
			
		||||
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -353,6 +353,19 @@ paths:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: "#/components/schemas/Person"
 | 
			
		||||
            examples:
 | 
			
		||||
              Update a person:
 | 
			
		||||
                value:
 | 
			
		||||
                  type: "person"
 | 
			
		||||
                  firstName: "string"
 | 
			
		||||
                  lastName: "string"
 | 
			
		||||
                  birthdate:
 | 
			
		||||
                    datetime: "2016-06-01T00:00:00+02:00"
 | 
			
		||||
                  deathdate:
 | 
			
		||||
                    datetime: "2021-06-01T00:00:00+02:00"
 | 
			
		||||
                  phonenumber: "string"
 | 
			
		||||
                  mobilenumber: "string"
 | 
			
		||||
                  gender: "male"
 | 
			
		||||
      responses:
 | 
			
		||||
        401:
 | 
			
		||||
          description: "Unauthorized"
 | 
			
		||||
@@ -375,6 +388,19 @@ paths:
 | 
			
		||||
          application/json:
 | 
			
		||||
            schema:
 | 
			
		||||
              $ref: "#/components/schemas/Person"
 | 
			
		||||
            examples:
 | 
			
		||||
              Create a new person:
 | 
			
		||||
                value:
 | 
			
		||||
                  type: "person"
 | 
			
		||||
                  firstName: "string"
 | 
			
		||||
                  lastName: "string"
 | 
			
		||||
                  birthdate:
 | 
			
		||||
                    datetime: "2016-06-01T00:00:00+02:00"
 | 
			
		||||
                  deathdate:
 | 
			
		||||
                    datetime: "2021-06-01T00:00:00+02:00"
 | 
			
		||||
                  phonenumber: "string"
 | 
			
		||||
                  mobilenumber: "string"
 | 
			
		||||
                  gender: "male"
 | 
			
		||||
      responses:
 | 
			
		||||
        200:
 | 
			
		||||
          description: "OK"
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user