mirror of
				https://gitlab.com/Chill-Projet/chill-bundles.git
				synced 2025-11-04 11:18:25 +00:00 
			
		
		
		
	Compare commits
	
		
			15 Commits
		
	
	
		
			405-aside-
			...
			#361-impro
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						caaed3e759
	
				 | 
					
					
						|||
| 
						
						
							
						
						6380fdd9a4
	
				 | 
					
					
						|||
| 
						
						
							
						
						fcd5080e6f
	
				 | 
					
					
						|||
| 
						
						
							
						
						086d418aa3
	
				 | 
					
					
						|||
| 
						
						
							
						
						4e61821e5b
	
				 | 
					
					
						|||
| 
						
						
							
						
						e3aeab315f
	
				 | 
					
					
						|||
| 
						
						
							
						
						8ec1063ef8
	
				 | 
					
					
						|||
| 
						
						
							
						
						aad9c984b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						34b3e290e1
	
				 | 
					
					
						|||
| 
						
						
							
						
						0987b575ab
	
				 | 
					
					
						|||
| 
						
						
							
						
						d960578c5f
	
				 | 
					
					
						|||
| 
						
						
							
						
						176048bce6
	
				 | 
					
					
						|||
| 
						
						
							
						
						be210a6dd6
	
				 | 
					
					
						|||
| 
						
						
							
						
						4323773595
	
				 | 
					
					
						|||
| 
						
						
							
						
						6d432ca2cb
	
				 | 
					
					
						
@@ -17,3 +17,9 @@ when@dev:
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/dev.assets.test2.html.twig'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    sass_address_picker:
 | 
			
		||||
        path: /_dev/address-picker
 | 
			
		||||
        controller: Symfony\Bundle\FrameworkBundle\Controller\TemplateController
 | 
			
		||||
        defaults:
 | 
			
		||||
            template: '@ChillMain/Dev/dev.address-picker.html.twig'
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,7 @@
 | 
			
		||||
    "webpack-cli": "^5.0.1"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@fragaria/address-formatter": "^6.6.1",
 | 
			
		||||
    "@fullcalendar/core": "^6.1.4",
 | 
			
		||||
    "@fullcalendar/daygrid": "^6.1.4",
 | 
			
		||||
    "@fullcalendar/interaction": "^6.1.4",
 | 
			
		||||
@@ -66,10 +67,12 @@
 | 
			
		||||
    "mime": "^4.0.0",
 | 
			
		||||
    "pdfjs-dist": "^4.3.136",
 | 
			
		||||
    "vis-network": "^9.1.0",
 | 
			
		||||
    "vue": "^3.5.6",
 | 
			
		||||
    "vue": "^3.5.x",
 | 
			
		||||
    "vue-i18n": "^9.1.6",
 | 
			
		||||
    "vue-multiselect": "3.0.0-alpha.2",
 | 
			
		||||
    "vue-toast-notification": "^3.1.2",
 | 
			
		||||
    "vue-tsc": "^3.1.0",
 | 
			
		||||
    "vue-use-leaflet": "^0.1.7",
 | 
			
		||||
    "vuex": "^4.0.0"
 | 
			
		||||
  },
 | 
			
		||||
  "browserslist": [
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,50 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class AddressReferenceAggregatedApiController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private Security $security,
 | 
			
		||||
        private AddressReferenceRepositoryInterface $addressReferenceRepository,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route(path: '/api/1.0/main/address-reference/aggregated/search')]
 | 
			
		||||
    public function search(Request $request): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
        if (!$this->security->isGranted('IS_AUTHENTICATED')) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!$request->query->has('q')) {
 | 
			
		||||
            throw new BadRequestHttpException('Parameter "q" is required.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $q = trim($request->query->get('q'));
 | 
			
		||||
 | 
			
		||||
        if ('' === $q) {
 | 
			
		||||
            throw new BadRequestHttpException('Parameter "q" is required and cannot be empty.');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $result = $this->addressReferenceRepository->findAggregatedBySearchString($q);
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse(iterator_to_array($result));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepositoryInterface;
 | 
			
		||||
use Symfony\Component\HttpFoundation\JsonResponse;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\Routing\Annotation\Route;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
final readonly class PostalCodeForAddressReferenceApiController
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(
 | 
			
		||||
        private PostalCodeForAddressReferenceRepositoryInterface $postalCodeForAddressReferenceRepository,
 | 
			
		||||
        private Security $security,
 | 
			
		||||
    ) {}
 | 
			
		||||
 | 
			
		||||
    #[Route('/api/1.0/main/address-reference/postal-code/search')]
 | 
			
		||||
    public function findPostalCodeBySearch(Request $request): JsonResponse
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
        if (!$this->security->isGranted('IS_AUTHENTICATED')) {
 | 
			
		||||
            throw new AccessDeniedHttpException();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $search = $request->query->get('q');
 | 
			
		||||
 | 
			
		||||
        if (null === $search || '' === trim($search)) {
 | 
			
		||||
            throw new BadRequestHttpException('No search query provided');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $postalCodes = iterator_to_array($this->postalCodeForAddressReferenceRepository->findPostalCode($search));
 | 
			
		||||
 | 
			
		||||
        return new JsonResponse($postalCodes, json: false);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -14,13 +14,14 @@ namespace Chill\MainBundle\Repository;
 | 
			
		||||
use Chill\MainBundle\Entity\AddressReference;
 | 
			
		||||
use Chill\MainBundle\Entity\PostalCode;
 | 
			
		||||
use Chill\MainBundle\Search\SearchApiQuery;
 | 
			
		||||
use Doctrine\DBAL\Types\Types;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Doctrine\ORM\EntityRepository;
 | 
			
		||||
use Doctrine\ORM\NativeQuery;
 | 
			
		||||
use Doctrine\ORM\Query\ResultSetMapping;
 | 
			
		||||
use Doctrine\ORM\Query\ResultSetMappingBuilder;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
final readonly class AddressReferenceRepository implements ObjectRepository
 | 
			
		||||
final readonly class AddressReferenceRepository implements AddressReferenceRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    private EntityManagerInterface $entityManager;
 | 
			
		||||
 | 
			
		||||
@@ -65,6 +66,121 @@ final readonly class AddressReferenceRepository implements ObjectRepository
 | 
			
		||||
        return $this->repository->findAll();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $terms = $this->buildTermsFromSearchString($search);
 | 
			
		||||
        if ([] === $terms) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $connection = $this->entityManager->getConnection();
 | 
			
		||||
        $qb = $connection->createQueryBuilder();
 | 
			
		||||
 | 
			
		||||
        $qb->select('row_number() OVER () AS row_number', 'var.street AS street', 'cmpc.id AS postcode_id', 'cmpc.code AS code', 'cmpc.label AS label', 'jsonb_object_agg(var.address_id, var.streetnumber ORDER BY var.row_number) AS positions')
 | 
			
		||||
            ->from('view_chill_main_address_reference', 'var')
 | 
			
		||||
            ->innerJoin('var', 'chill_main_postal_code', 'cmpc', 'cmpc.id = var.postcode_id')
 | 
			
		||||
            ->groupBy('cmpc.id', 'var.street')
 | 
			
		||||
            ->setFirstResult($firstResult)
 | 
			
		||||
            ->setMaxResults($maxResults);
 | 
			
		||||
 | 
			
		||||
        $paramId = 0;
 | 
			
		||||
 | 
			
		||||
        foreach ($terms as $term) {
 | 
			
		||||
            $qb->andWhere('var.address like UNACCENT(LOWER(?))');
 | 
			
		||||
            $qb->setParameter(++$paramId, "%{$term}%");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $postalCode) {
 | 
			
		||||
            $qb->andWhere('var.postcode_id = ?');
 | 
			
		||||
            $qb->setParameter(++$paramId, $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $result = $qb->executeQuery();
 | 
			
		||||
 | 
			
		||||
        foreach ($result->iterateAssociative() as $row) {
 | 
			
		||||
            yield [...$row, 'positions' => json_decode($row['positions'], true, 512, JSON_THROW_ON_ERROR)];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return iterable<AddressReference>
 | 
			
		||||
     */
 | 
			
		||||
    public function findBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $terms = $this->buildTermsFromSearchString($search);
 | 
			
		||||
        if ([] === $terms) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rsm = new ResultSetMappingBuilder($this->entityManager);
 | 
			
		||||
        $rsm->addRootEntityFromClassMetadata(AddressReference::class, 'ar');
 | 
			
		||||
        $baseSql = 'SELECT '.$rsm->generateSelectClause(['ar' => 'ar']).' FROM chill_main_address_reference ar JOIN
 | 
			
		||||
            view_chill_main_address_reference var ON var.address_id = ar.id';
 | 
			
		||||
        $nql = $this->buildQueryBySearchString($rsm, $baseSql, $terms, $postalCode);
 | 
			
		||||
 | 
			
		||||
        $orderBy = [];
 | 
			
		||||
        $pertinence = [];
 | 
			
		||||
        foreach ($terms as $k => $term) {
 | 
			
		||||
            $pertinence[] =
 | 
			
		||||
                "(EXISTS (SELECT 1 FROM unnest(string_to_array(address, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(:order{$k})))))::int";
 | 
			
		||||
            $pertinence[] = "(address LIKE UNACCENT(LOWER(:order{$k})))::int";
 | 
			
		||||
            $nql->setParameter('order'.$k, $term);
 | 
			
		||||
        }
 | 
			
		||||
        $orderBy[] = implode(' + ', $pertinence).' ASC';
 | 
			
		||||
        $orderBy[] = implode('row_number ASC', $orderBy);
 | 
			
		||||
 | 
			
		||||
        $nql->setSQL($nql->getSQL().' ORDER BY '.implode(', ', $orderBy));
 | 
			
		||||
 | 
			
		||||
        return $nql->toIterable();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function countBySearchString(string $search, PostalCode|int|null $postalCode = null): int
 | 
			
		||||
    {
 | 
			
		||||
        $terms = $this->buildTermsFromSearchString($search);
 | 
			
		||||
        if ([] === $terms) {
 | 
			
		||||
            return 0;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $rsm = new ResultSetMappingBuilder($this->entityManager);
 | 
			
		||||
        $rsm->addScalarResult('c', 'c', Types::INTEGER);
 | 
			
		||||
        $nql = $this->buildQueryBySearchString($rsm, 'SELECT COUNT(var.*) AS c FROM view_chill_main_address_reference var', $terms, $postalCode);
 | 
			
		||||
 | 
			
		||||
        return $nql->getSingleScalarResult();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildTermsFromSearchString(string $search): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_filter(
 | 
			
		||||
            array_map(
 | 
			
		||||
                static fn (string $term) => trim($term),
 | 
			
		||||
                explode(' ', $search)
 | 
			
		||||
            ),
 | 
			
		||||
            static fn (string $term) => '' !== $term
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildQueryBySearchString(ResultSetMapping $rsm, string $select, array $terms, PostalCode|int|null $postalCode = null): NativeQuery
 | 
			
		||||
    {
 | 
			
		||||
        $nql = $this->entityManager->createNativeQuery('', $rsm);
 | 
			
		||||
 | 
			
		||||
        $sql = $select.' WHERE ';
 | 
			
		||||
 | 
			
		||||
        $wheres = [];
 | 
			
		||||
        foreach ($terms as $k => $term) {
 | 
			
		||||
            $wheres[] = "var.address like :w{$k}";
 | 
			
		||||
            $nql->setParameter("w{$k}", '%'.$term.'%', Types::STRING);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (null !== $postalCode) {
 | 
			
		||||
            $wheres[] = 'var.postcode_id = :postalCode';
 | 
			
		||||
            $nql->setParameter('postalCode', $postalCode instanceof PostalCode ? $postalCode->getId() : $postalCode);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $nql->setSQL($sql.implode(' AND ', $wheres));
 | 
			
		||||
 | 
			
		||||
        return $nql;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @param mixed|null $limit
 | 
			
		||||
     * @param mixed|null $offset
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\PostalCode;
 | 
			
		||||
use Doctrine\Persistence\ObjectRepository;
 | 
			
		||||
 | 
			
		||||
interface AddressReferenceRepositoryInterface extends ObjectRepository
 | 
			
		||||
{
 | 
			
		||||
    public function findAggregatedBySearchString(string $search, PostalCode|int|null $postalCode = null, int $firstResult = 0, int $maxResults = 50): iterable;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Repository;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
 | 
			
		||||
final readonly class PostalCodeForAddressReferenceRepository implements PostalCodeForAddressReferenceRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    public function __construct(private Connection $connection) {}
 | 
			
		||||
 | 
			
		||||
    public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable
 | 
			
		||||
    {
 | 
			
		||||
        $terms = $this->buildTermsFromSearchString($search);
 | 
			
		||||
        if ([] === $terms) {
 | 
			
		||||
            return [];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $qb = $this->connection->createQueryBuilder();
 | 
			
		||||
 | 
			
		||||
        $qb->from('chill_main_postal_code', 'cmpc')
 | 
			
		||||
            ->join('cmpc', 'view_chill_main_address_reference', 'vcmar', 'vcmar.postcode_id = cmpc.id')
 | 
			
		||||
            ->join('vcmar', 'country', 'country', condition: 'cmpc.country_id = country.id')
 | 
			
		||||
            ->setFirstResult($firstResult)
 | 
			
		||||
            ->setMaxResults($maxResults)
 | 
			
		||||
        ;
 | 
			
		||||
 | 
			
		||||
        $qb->select(
 | 
			
		||||
            'DISTINCT ON (cmpc.code, cmpc.label) cmpc.id AS postcode_id',
 | 
			
		||||
            'cmpc.code AS code',
 | 
			
		||||
            'cmpc.label AS label',
 | 
			
		||||
            'country.id AS country_id',
 | 
			
		||||
            'country.countrycode AS country_code',
 | 
			
		||||
            'country.name AS country_name'
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        $paramId = 0;
 | 
			
		||||
 | 
			
		||||
        foreach ($terms as $term) {
 | 
			
		||||
            $qb->andWhere('vcmar.address like ?');
 | 
			
		||||
            $qb->setParameter(++$paramId, "%{$term}%");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        $result = $qb->executeQuery();
 | 
			
		||||
 | 
			
		||||
        foreach ($result->iterateAssociative() as $row) {
 | 
			
		||||
            yield [...$row, 'country_name' => json_decode($row['country_name'], true, 512, JSON_THROW_ON_ERROR)];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private function buildTermsFromSearchString(string $search): array
 | 
			
		||||
    {
 | 
			
		||||
        return array_filter(
 | 
			
		||||
            array_map(
 | 
			
		||||
                static fn (string $term) => trim($term),
 | 
			
		||||
                explode(' ', $search)
 | 
			
		||||
            ),
 | 
			
		||||
            static fn (string $term) => '' !== $term
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,20 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Repository;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Search for postal code using optimized materialized view.
 | 
			
		||||
 */
 | 
			
		||||
interface PostalCodeForAddressReferenceRepositoryInterface
 | 
			
		||||
{
 | 
			
		||||
    public function findPostalCode(string $search, int $firstResult = 0, int $maxResults = 50): iterable;
 | 
			
		||||
}
 | 
			
		||||
@@ -75,6 +75,7 @@ export interface Postcode {
 | 
			
		||||
    name: string;
 | 
			
		||||
    code: string;
 | 
			
		||||
    center: Point;
 | 
			
		||||
    country: Country;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Point {
 | 
			
		||||
@@ -90,6 +91,28 @@ export interface Country {
 | 
			
		||||
 | 
			
		||||
export type AddressRefStatus = "match" | "to_review" | "reviewed";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An interface to create an address
 | 
			
		||||
 */
 | 
			
		||||
export interface AddressCreation {
 | 
			
		||||
    confidential: boolean;
 | 
			
		||||
    isNoAddress: boolean;
 | 
			
		||||
    street: string;
 | 
			
		||||
    streetNumber: string;
 | 
			
		||||
    postcode: Postcode;
 | 
			
		||||
    point: Point; // [number, number]; // [longitude, latitude]
 | 
			
		||||
    addressReference: AddressReference;
 | 
			
		||||
    validFrom: DateTime|null;
 | 
			
		||||
    floor: string;
 | 
			
		||||
    corridor: string;
 | 
			
		||||
    steps: string;
 | 
			
		||||
    flat: string;
 | 
			
		||||
    buildingName: string;
 | 
			
		||||
    distribution: string;
 | 
			
		||||
    extra: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export interface Address {
 | 
			
		||||
    type: "address";
 | 
			
		||||
    address_id: number;
 | 
			
		||||
@@ -108,7 +131,7 @@ export interface Address {
 | 
			
		||||
    confidential: boolean;
 | 
			
		||||
    lines: string[];
 | 
			
		||||
    addressReference: AddressReference | null;
 | 
			
		||||
    validFrom: DateTime;
 | 
			
		||||
    validFrom: DateTime  | null; // TODO there is no null for validFrom
 | 
			
		||||
    validTo: DateTime | null;
 | 
			
		||||
    point: Point | null;
 | 
			
		||||
    refStatus: AddressRefStatus;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,33 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 | 
			
		||||
import Modal from "ChillMainAssets/vuejs/_components/Modal.vue";
 | 
			
		||||
import AddressPicker from "ChillMainAssets/vuejs/AddressPicker/AddressPicker.vue";
 | 
			
		||||
import {Ref, ref} from "vue";
 | 
			
		||||
 | 
			
		||||
const showModal: Ref<boolean> = ref(false);
 | 
			
		||||
 | 
			
		||||
const modalDialogClasses = {"modal-dialog": true, "modal-dialog-scrollable": true, "modal-xl": true};
 | 
			
		||||
 | 
			
		||||
const clickButton = () => {
 | 
			
		||||
    showModal.value = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const closeModal = () => {
 | 
			
		||||
    showModal.value = false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <modal v-if="showModal" :hide-footer="false" :modal-dialog-class="modalDialogClasses" @close="closeModal">
 | 
			
		||||
        <template v-slot:header>TODO</template>
 | 
			
		||||
        <template v-slot:body>
 | 
			
		||||
            <AddressPicker></AddressPicker>
 | 
			
		||||
        </template>
 | 
			
		||||
    </modal>
 | 
			
		||||
    <button class="btn btn-submit" type="button" @click="clickButton">SEARCH ADDRESS</button>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,170 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {Address, AddressReference} from "ChillMainAssets/types";
 | 
			
		||||
import SearchBar from "ChillMainAssets/vuejs/AddressPicker/Component/SearchBar.vue";
 | 
			
		||||
import {
 | 
			
		||||
    AddressAggregated,
 | 
			
		||||
    AssociatedPostalCode, fetchAddressReference,
 | 
			
		||||
    getAddressesAggregated,
 | 
			
		||||
    getPostalCodes,
 | 
			
		||||
} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
 | 
			
		||||
import {computed, Ref, ref} from "vue";
 | 
			
		||||
import AddressAggregatedList from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedList.vue";
 | 
			
		||||
import AddressDetailsForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressDetailsForm.vue";
 | 
			
		||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
 | 
			
		||||
import {trans, SAVE} from "translator";
 | 
			
		||||
 | 
			
		||||
interface AddressPickerProps {
 | 
			
		||||
    suggestions?: Address[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<AddressPickerProps>(), {
 | 
			
		||||
    suggestions: () => [],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const addresses: Ref<AddressAggregated[]> = ref([]);
 | 
			
		||||
const postalCodes: Ref<AssociatedPostalCode[]> = ref([]);
 | 
			
		||||
const searchTokens: Ref<string[]> = ref([]);
 | 
			
		||||
const addressReference: Ref<AddressReference|null> = ref(null);
 | 
			
		||||
 | 
			
		||||
let abortControllerSearchAddress: null | AbortController = null;
 | 
			
		||||
let abortControllerSearchPostalCode: null | AbortController = null;
 | 
			
		||||
 | 
			
		||||
const searchResultsClasses = computed(() => ({
 | 
			
		||||
    "mid-size": addressReference !== null,
 | 
			
		||||
}));
 | 
			
		||||
 | 
			
		||||
const floor = ref<string>("");
 | 
			
		||||
const corridor = ref<string>("");
 | 
			
		||||
const steps = ref<string>("");
 | 
			
		||||
const flat = ref<string>("");
 | 
			
		||||
const buildingName = ref<string>("");
 | 
			
		||||
const extra = ref<string>("");
 | 
			
		||||
const distribution = ref<string>("");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const onSearch = async function (search: string): Promise<void> {
 | 
			
		||||
    performSearchForAddress(search);
 | 
			
		||||
    performSearchForPostalCode(search);
 | 
			
		||||
    searchTokens.value = [search];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const onPickPosition = async (id: string) => {
 | 
			
		||||
    console.log('Pick Position', id);
 | 
			
		||||
    addressReference.value = await fetchAddressReference(id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const performSearchForAddress = async (search: string): Promise<void> => {
 | 
			
		||||
    if (null !== abortControllerSearchAddress) {
 | 
			
		||||
        abortControllerSearchAddress.abort();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ("" === search) {
 | 
			
		||||
        addresses.value = [];
 | 
			
		||||
        abortControllerSearchAddress = null;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abortControllerSearchAddress = new AbortController();
 | 
			
		||||
 | 
			
		||||
    console.log("onSearch", search);
 | 
			
		||||
    try {
 | 
			
		||||
        addresses.value = await getAddressesAggregated(
 | 
			
		||||
            search,
 | 
			
		||||
            abortControllerSearchAddress,
 | 
			
		||||
        );
 | 
			
		||||
        abortControllerSearchAddress = null;
 | 
			
		||||
 | 
			
		||||
        // check if there is only one result
 | 
			
		||||
        if (addresses.value.length === 1 && Object.keys(addresses.value[0].positions).length === 1) {
 | 
			
		||||
            onPickPosition(Object.keys(addresses.value[0].positions)[0]);
 | 
			
		||||
        }
 | 
			
		||||
    } catch (e: unknown) {
 | 
			
		||||
        if (e instanceof DOMException && e.name === "AbortError") {
 | 
			
		||||
            console.log("search aborted for:", search);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw e;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const performSearchForPostalCode = async (search: string): Promise<void> => {
 | 
			
		||||
    if (null !== abortControllerSearchPostalCode) {
 | 
			
		||||
        abortControllerSearchPostalCode.abort();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if ("" === search) {
 | 
			
		||||
        addresses.value = [];
 | 
			
		||||
        abortControllerSearchPostalCode = null;
 | 
			
		||||
        return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    abortControllerSearchPostalCode = new AbortController();
 | 
			
		||||
 | 
			
		||||
    console.log("onSearch", search);
 | 
			
		||||
    try {
 | 
			
		||||
        postalCodes.value = await getPostalCodes(
 | 
			
		||||
            search,
 | 
			
		||||
            abortControllerSearchPostalCode,
 | 
			
		||||
        );
 | 
			
		||||
        abortControllerSearchPostalCode = null;
 | 
			
		||||
    } catch (e: unknown) {
 | 
			
		||||
        if (e instanceof DOMException && e.name === "AbortError") {
 | 
			
		||||
            console.log("search postal code aborted for:", search);
 | 
			
		||||
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        throw e;
 | 
			
		||||
    }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const save = async(): Promise<void> => {
 | 
			
		||||
    console.log("save");
 | 
			
		||||
    console.log("content", floor, corridor, steps, flat, buildingName, extra, distribution);
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <search-bar @search="onSearch"></search-bar>
 | 
			
		||||
    <div class="address-pick-content">
 | 
			
		||||
        <div class="search-results" :class="searchResultsClasses">
 | 
			
		||||
            <address-aggregated-list :addresses="addresses" :search-tokens="searchTokens" @pick-position="(id) => onPickPosition(id)"></address-aggregated-list>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div v-if="addressReference !== null" class="address-details-form">
 | 
			
		||||
            <address-details-form :address="addressReference"
 | 
			
		||||
                                  v-model:floor="floor"
 | 
			
		||||
                                  v-model:corridor="corridor"
 | 
			
		||||
                                  v-model:steps="steps"
 | 
			
		||||
                                  v-model:flat="flat"
 | 
			
		||||
                                  v-model:building-name="buildingName"
 | 
			
		||||
                                  v-model:extra="extra"
 | 
			
		||||
                                  v-model:distribution="distribution"
 | 
			
		||||
            />
 | 
			
		||||
        </div>
 | 
			
		||||
        <div>
 | 
			
		||||
            <ul class="record_actions">
 | 
			
		||||
                <li><button class="btn btn-save">{{ trans(SAVE) }}</button></li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.address-pick-content {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    gap: 1rem;
 | 
			
		||||
 | 
			
		||||
    .search-results {
 | 
			
		||||
        &.mid-size {
 | 
			
		||||
            width: 50%;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    .address-details-form {
 | 
			
		||||
        width: 50%;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,25 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
 | 
			
		||||
import AddressAggregatedListItem from "ChillMainAssets/vuejs/AddressPicker/Component/AddressAggregatedListItem.vue";
 | 
			
		||||
 | 
			
		||||
interface AddressAggregatedListProps {
 | 
			
		||||
    addresses: AddressAggregated[];
 | 
			
		||||
    searchTokens: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<AddressAggregatedListProps>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    pickPosition: [id: string]
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <template v-for="a in props.addresses" :key="a.row_number">
 | 
			
		||||
        <address-aggregated-list-item :address="a" :search-tokens="props.searchTokens" @pick-position="(id) => emit('pickPosition', id)"></address-aggregated-list-item>
 | 
			
		||||
    </template>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,82 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {AddressAggregated} from "ChillMainAssets/vuejs/AddressPicker/driver/local-search";
 | 
			
		||||
import {computed, ref} from "vue";
 | 
			
		||||
 | 
			
		||||
interface AddressAggregatedListItemProps {
 | 
			
		||||
    address: AddressAggregated;
 | 
			
		||||
    searchTokens: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<AddressAggregatedListItemProps>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
    pickPosition: [id: string]
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const showAllPositions = ref<boolean>(false);
 | 
			
		||||
const positionsToShow = computed((): Record<string, string> => {
 | 
			
		||||
    const obj: Record<string, any> = {};
 | 
			
		||||
    let count = 0;
 | 
			
		||||
    for (const [id, position] of Object.entries(props.address.positions)) {
 | 
			
		||||
       obj[id] = position;
 | 
			
		||||
       count++;
 | 
			
		||||
       if (count >= 10 && !showAllPositions.value) {
 | 
			
		||||
           break;
 | 
			
		||||
       }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return obj;
 | 
			
		||||
})
 | 
			
		||||
const needToShowMorePosition = computed(() => {
 | 
			
		||||
    return Object.keys(props.address.positions).length > 10;
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const onClickButton = (id: string) => {
 | 
			
		||||
    console.log('onClickButton', id);
 | 
			
		||||
    emit('pickPosition', id);
 | 
			
		||||
}
 | 
			
		||||
const displayAllPositions = () => {
 | 
			
		||||
    showAllPositions.value = true;
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <div class="street">
 | 
			
		||||
            <span>{{ props.address.street }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="postcode">
 | 
			
		||||
            <span>{{ props.address.code }}</span> <span>{{ address.label }}</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="positions">
 | 
			
		||||
            <ul>
 | 
			
		||||
                <li v-for="(position, id) in positionsToShow" :key="id"  >
 | 
			
		||||
                    <button type="button" @click="onClickButton(id)"  >
 | 
			
		||||
                        {{ position }}
 | 
			
		||||
                    </button>
 | 
			
		||||
                </li>
 | 
			
		||||
                <li v-if="needToShowMorePosition">
 | 
			
		||||
                    <button @click="displayAllPositions">show all</button>
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
.street {
 | 
			
		||||
    font-variant: small-caps;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
.postcode {
 | 
			
		||||
    font-variant: small-caps;
 | 
			
		||||
}
 | 
			
		||||
.positions ul {
 | 
			
		||||
    list-style-type: none;
 | 
			
		||||
 | 
			
		||||
    li {
 | 
			
		||||
        display: inline-block;
 | 
			
		||||
        margin-right: 2px;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {AddressReference} from "ChillMainAssets/types";
 | 
			
		||||
import {computed, ref} from "vue";
 | 
			
		||||
import {addressReferenceToAddress} from "ChillMainAssets/vuejs/AddressPicker/helper";
 | 
			
		||||
import AddressDetailsContent from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsContent.vue";
 | 
			
		||||
import AddressForm from "ChillMainAssets/vuejs/AddressPicker/Component/AddressForm.vue";
 | 
			
		||||
 | 
			
		||||
export interface AddressDetailsFormProps {
 | 
			
		||||
    address: AddressReference;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<AddressDetailsFormProps>();
 | 
			
		||||
 | 
			
		||||
const floor = ref<string>("");
 | 
			
		||||
const corridor = ref<string>("");
 | 
			
		||||
const steps = ref<string>("");
 | 
			
		||||
const flat = ref<string>("");
 | 
			
		||||
const buildingName = ref<string>("");
 | 
			
		||||
const extra = ref<string>("");
 | 
			
		||||
const distribution = ref<string>("");
 | 
			
		||||
 | 
			
		||||
const address = computed(() => addressReferenceToAddress(props.address));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
        <address-form
 | 
			
		||||
            @update:floor="val => (floor = val)"
 | 
			
		||||
            @update:corridor="val => (corridor = val)"
 | 
			
		||||
            @update:steps="val => (steps = val)"
 | 
			
		||||
            @update:flat="val => (flat = val)"
 | 
			
		||||
            @update:building-name="val => (buildingName = val)"
 | 
			
		||||
            @update:extra="val => (extra = val)"
 | 
			
		||||
            @update:distribution="val => (distribution = val)"
 | 
			
		||||
        ></address-form>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div>
 | 
			
		||||
        <address-details-content :address="address"></address-details-content>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,112 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import {
 | 
			
		||||
    ADDRESS_STREET,
 | 
			
		||||
    ADDRESS_STREET_NUMBER,
 | 
			
		||||
    ADDRESS_FLOOR,
 | 
			
		||||
    ADDRESS_CORRIDOR,
 | 
			
		||||
    ADDRESS_STEPS,
 | 
			
		||||
    ADDRESS_FLAT,
 | 
			
		||||
    ADDRESS_BUILDING_NAME,
 | 
			
		||||
    ADDRESS_DISTRIBUTION,
 | 
			
		||||
    ADDRESS_EXTRA,
 | 
			
		||||
    ADDRESS_FILL_AN_ADDRESS,
 | 
			
		||||
    trans,
 | 
			
		||||
} from "translator";
 | 
			
		||||
import {ref} from "vue";
 | 
			
		||||
 | 
			
		||||
const isNoAddress = ref(false);
 | 
			
		||||
 | 
			
		||||
const floor = defineModel("floor");
 | 
			
		||||
const corridor = defineModel("corridor");
 | 
			
		||||
const steps = defineModel("steps");
 | 
			
		||||
const flat = defineModel("flat");
 | 
			
		||||
const buildingName = defineModel("buildingName");
 | 
			
		||||
const extra = defineModel("extra");
 | 
			
		||||
const distribution = defineModel("distribution");
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="form-floating my-1">
 | 
			
		||||
        <input
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            type="text"
 | 
			
		||||
            name="floor"
 | 
			
		||||
            :placeholder="trans(ADDRESS_FLOOR)"
 | 
			
		||||
            v-model="floor"
 | 
			
		||||
        />
 | 
			
		||||
        <label for="floor">{{ trans(ADDRESS_FLOOR) }}</label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-floating my-1">
 | 
			
		||||
        <input
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            type="text"
 | 
			
		||||
            name="corridor"
 | 
			
		||||
            :placeholder="trans(ADDRESS_CORRIDOR)"
 | 
			
		||||
            v-model="corridor"
 | 
			
		||||
        />
 | 
			
		||||
        <label for="corridor">{{ trans(ADDRESS_CORRIDOR) }}</label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-floating my-1">
 | 
			
		||||
        <input
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            type="text"
 | 
			
		||||
            name="steps"
 | 
			
		||||
            :placeholder="trans(ADDRESS_STEPS)"
 | 
			
		||||
            v-model="steps"
 | 
			
		||||
        />
 | 
			
		||||
        <label for="steps">{{ trans(ADDRESS_STEPS) }}</label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="form-floating my-1">
 | 
			
		||||
        <input
 | 
			
		||||
            class="form-control"
 | 
			
		||||
            type="text"
 | 
			
		||||
            name="flat"
 | 
			
		||||
            :placeholder="trans(ADDRESS_FLAT)"
 | 
			
		||||
            v-model="flat"
 | 
			
		||||
        />
 | 
			
		||||
        <label for="flat">{{ trans(ADDRESS_FLAT) }}</label>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div :class="isNoAddress ? 'col-lg-12' : 'col-lg-6'">
 | 
			
		||||
        <div class="form-floating my-1" v-if="!isNoAddress">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control"
 | 
			
		||||
                type="text"
 | 
			
		||||
                name="buildingName"
 | 
			
		||||
                maxlength="255"
 | 
			
		||||
                :placeholder="trans(ADDRESS_BUILDING_NAME)"
 | 
			
		||||
                v-model="buildingName"
 | 
			
		||||
            />
 | 
			
		||||
            <label for="buildingName">{{
 | 
			
		||||
                    trans(ADDRESS_BUILDING_NAME)
 | 
			
		||||
                }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-floating my-1">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control"
 | 
			
		||||
                type="text"
 | 
			
		||||
                name="extra"
 | 
			
		||||
                maxlength="255"
 | 
			
		||||
                :placeholder="trans(ADDRESS_EXTRA)"
 | 
			
		||||
                v-model="extra"
 | 
			
		||||
            />
 | 
			
		||||
            <label for="extra">{{ trans(ADDRESS_EXTRA) }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="form-floating my-1" v-if="!isNoAddress">
 | 
			
		||||
            <input
 | 
			
		||||
                class="form-control"
 | 
			
		||||
                type="text"
 | 
			
		||||
                name="distribution"
 | 
			
		||||
                maxlength="255"
 | 
			
		||||
                :placeholder="trans(ADDRESS_DISTRIBUTION)"
 | 
			
		||||
                v-model="distribution"
 | 
			
		||||
            />
 | 
			
		||||
            <label for="distribution">{{
 | 
			
		||||
                    trans(ADDRESS_DISTRIBUTION)
 | 
			
		||||
                }}</label>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss">
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -0,0 +1,39 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ADDRESS_PICKER_SEARCH_FOR_ADDRESSES, trans } from 'translator';
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
    search: [search: string];
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let searchTimer = 0;
 | 
			
		||||
let searchString: string;
 | 
			
		||||
 | 
			
		||||
const onInput = function (event: InputEvent) {
 | 
			
		||||
    const target = event.target as HTMLInputElement;
 | 
			
		||||
    const value = target.value;
 | 
			
		||||
    searchString = value;
 | 
			
		||||
 | 
			
		||||
    if (0 === searchTimer) {
 | 
			
		||||
        window.clearTimeout(searchTimer);
 | 
			
		||||
        searchTimer = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    searchTimer = window.setTimeout(() => {
 | 
			
		||||
        if (value === searchString) {
 | 
			
		||||
            emits("search", value);
 | 
			
		||||
        }
 | 
			
		||||
    }, 500);
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
    <div class="input-group mb-3">
 | 
			
		||||
        <span class="input-group-text">
 | 
			
		||||
            <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-search" viewBox="0 0 16 16">
 | 
			
		||||
              <path d="M11.742 10.344a6.5 6.5 0 1 0-1.397 1.398h-.001q.044.06.098.115l3.85 3.85a1 1 0 0 0 1.415-1.414l-3.85-3.85a1 1 0 0 0-.115-.1zM12 6.5a5.5 5.5 0 1 1-11 0 5.5 5.5 0 0 1 11 0"/>
 | 
			
		||||
            </svg>
 | 
			
		||||
        </span>
 | 
			
		||||
        <input type="search" class="form-control" @input="onInput" :placeholder="trans(ADDRESS_PICKER_SEARCH_FOR_ADDRESSES)" />
 | 
			
		||||
    </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped lang="scss"></style>
 | 
			
		||||
@@ -0,0 +1,69 @@
 | 
			
		||||
import {AddressReference, TranslatableString} from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
export interface AddressAggregated {
 | 
			
		||||
    row_number: number;
 | 
			
		||||
    street: string;
 | 
			
		||||
    postcode_id: number;
 | 
			
		||||
    code: string;
 | 
			
		||||
    label: string;
 | 
			
		||||
    positions: Record<string, string>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AssociatedPostalCode {
 | 
			
		||||
    postcode_id: number;
 | 
			
		||||
    code: string;
 | 
			
		||||
    label: string;
 | 
			
		||||
    country_id: number;
 | 
			
		||||
    country_code: string;
 | 
			
		||||
    country_name: TranslatableString;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @throws {DOMException} when fetch is aborted, the property name is always equals to 'AbortError'
 | 
			
		||||
 */
 | 
			
		||||
export const getAddressesAggregated = async (
 | 
			
		||||
    search: string,
 | 
			
		||||
    abortController: AbortController,
 | 
			
		||||
): Promise<AddressAggregated[]> => {
 | 
			
		||||
    const params = new URLSearchParams({ q: search.trim() });
 | 
			
		||||
    let response = null;
 | 
			
		||||
 | 
			
		||||
    response = await fetch(
 | 
			
		||||
        `/api/1.0/main/address-reference/aggregated/search?${params}`,
 | 
			
		||||
        { signal: abortController.signal },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
        return await response.json();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new Error(response.statusText);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getPostalCodes = async (
 | 
			
		||||
    search: string,
 | 
			
		||||
    abortController: AbortController,
 | 
			
		||||
): Promise<AssociatedPostalCode[]> => {
 | 
			
		||||
    const params = new URLSearchParams({ q: search.trim() });
 | 
			
		||||
    let response = null;
 | 
			
		||||
 | 
			
		||||
    response = await fetch(
 | 
			
		||||
        `/api/1.0/main/address-reference/postal-code/search?${params}`,
 | 
			
		||||
        { signal: abortController.signal },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
        return await response.json();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new Error(response.statusText);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const fetchAddressReference = async (id: string): Promise<AddressReference> => {
 | 
			
		||||
    const response = await fetch(`/api/1.0/main/address-reference/${id}.json`);
 | 
			
		||||
    if (response.ok) {
 | 
			
		||||
        return await response.json();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    throw new Error(response.statusText);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,21 @@
 | 
			
		||||
import {Address, AddressCreation, AddressReference} from "ChillMainAssets/types";
 | 
			
		||||
 | 
			
		||||
export const addressReferenceToAddress = (reference: AddressReference): AddressCreation => {
 | 
			
		||||
    return {
 | 
			
		||||
        street: reference.street,
 | 
			
		||||
        streetNumber: reference.streetNumber,
 | 
			
		||||
        postcode: reference.postcode,
 | 
			
		||||
        floor: "",
 | 
			
		||||
        corridor: "",
 | 
			
		||||
        steps: "",
 | 
			
		||||
        flat: "",
 | 
			
		||||
        buildingName: "",
 | 
			
		||||
        distribution: "",
 | 
			
		||||
        extra: "",
 | 
			
		||||
        confidential: false,
 | 
			
		||||
        addressReference: reference,
 | 
			
		||||
        point: reference.point,
 | 
			
		||||
        isNoAddress: false,
 | 
			
		||||
        validFrom: null,
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
import { createApp } from "vue";
 | 
			
		||||
import AddressButton from "ChillMainAssets/vuejs/AddressPicker/AddressButton.vue";
 | 
			
		||||
 | 
			
		||||
document.addEventListener("DOMContentLoaded", async () => {
 | 
			
		||||
    document
 | 
			
		||||
        .querySelectorAll<HTMLDivElement>("div[data-address-picker]")
 | 
			
		||||
        .forEach((elem): void => {
 | 
			
		||||
            const app = createApp(AddressButton);
 | 
			
		||||
 | 
			
		||||
            app.mount(elem);
 | 
			
		||||
        });
 | 
			
		||||
});
 | 
			
		||||
@@ -4,24 +4,27 @@
 | 
			
		||||
        :show-button-details="false"
 | 
			
		||||
    ></address-render-box>
 | 
			
		||||
    <address-details-ref-matching
 | 
			
		||||
        v-if="isAddress(props.address)"
 | 
			
		||||
        :address="props.address"
 | 
			
		||||
        @update-address="onUpdateAddress"
 | 
			
		||||
    ></address-details-ref-matching>
 | 
			
		||||
    <address-details-map :address="props.address"></address-details-map>
 | 
			
		||||
    <address-details-geographical-layers
 | 
			
		||||
        v-if="isAddress(props.address)"
 | 
			
		||||
        :address="props.address"
 | 
			
		||||
    ></address-details-geographical-layers>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { Address } from "../../../types";
 | 
			
		||||
import {Address, AddressCreation} from "../../../types";
 | 
			
		||||
import AddressDetailsMap from "./Parts/AddressDetailsMap.vue";
 | 
			
		||||
import AddressRenderBox from "../Entity/AddressRenderBox.vue";
 | 
			
		||||
import AddressDetailsGeographicalLayers from "./Parts/AddressDetailsGeographicalLayers.vue";
 | 
			
		||||
import AddressDetailsRefMatching from "./Parts/AddressDetailsRefMatching.vue";
 | 
			
		||||
import {isAddress} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
 | 
			
		||||
 | 
			
		||||
interface AddressModalContentProps {
 | 
			
		||||
    address: Address;
 | 
			
		||||
    address: Address|AddressCreation;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<AddressModalContentProps>();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,90 +12,91 @@
 | 
			
		||||
        Voir sur
 | 
			
		||||
        <a :href="makeUrlGoogleMap(props.address)" target="_blank"
 | 
			
		||||
            >Google Maps</a
 | 
			
		||||
        >
 | 
			
		||||
        <a :href="makeUrlOsm(props.address)" target="_blank">OSM</a>
 | 
			
		||||
        > <a
 | 
			
		||||
          :href="makeUrlOsm(props.address)" target="_blank"
 | 
			
		||||
        >OSM</a>
 | 
			
		||||
    </p>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
import {computed, onMounted, ref} from "vue";
 | 
			
		||||
import "leaflet/dist/leaflet.css";
 | 
			
		||||
import markerIconPng from "leaflet/dist/images/marker-icon.png";
 | 
			
		||||
import L, { LatLngExpression, LatLngTuple } from "leaflet";
 | 
			
		||||
import { Address, Point } from "../../../../types";
 | 
			
		||||
import {Address, AddressCreation, Point} from "../../../../types";
 | 
			
		||||
import {buildAddressLines, getAddressPoint} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
 | 
			
		||||
import {useLeafletDisplayLayer, useLeafletMap, useLeafletMarker, useLeafletTileLayer} from "vue-use-leaflet";
 | 
			
		||||
 | 
			
		||||
const lonLatForLeaflet = (point: Point): LatLngTuple => {
 | 
			
		||||
    return [point.coordinates[1], point.coordinates[0]];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface MapProps {
 | 
			
		||||
    address: Address;
 | 
			
		||||
    address: Address|AddressCreation;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const props = defineProps<MapProps>();
 | 
			
		||||
 | 
			
		||||
const map_div = ref<HTMLDivElement | null>(null);
 | 
			
		||||
let map: L.Map | null = null;
 | 
			
		||||
let marker: L.Marker | null = null;
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
    if (map_div.value === null) {
 | 
			
		||||
        // there is no map div when the address does not have any Point
 | 
			
		||||
        return;
 | 
			
		||||
const markerIcon = L.icon({
 | 
			
		||||
    iconUrl: markerIconPng,
 | 
			
		||||
    iconAnchor: [12, 41],
 | 
			
		||||
});
 | 
			
		||||
const latLngMarker = computed((): LatLngExpression => {
 | 
			
		||||
    if (props.address === null || props.address.point === null) {
 | 
			
		||||
        return [0, 0, 0];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (props.address.point !== null) {
 | 
			
		||||
        map = L.map(map_div.value);
 | 
			
		||||
        map.setView(lonLatForLeaflet(props.address.point), 18);
 | 
			
		||||
 | 
			
		||||
        L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
 | 
			
		||||
            attribution:
 | 
			
		||||
                '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
 | 
			
		||||
        }).addTo(map);
 | 
			
		||||
 | 
			
		||||
        const markerIcon = L.icon({
 | 
			
		||||
            iconUrl: markerIconPng,
 | 
			
		||||
            iconAnchor: [12, 41],
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        marker = L.marker(lonLatForLeaflet(props.address.point), {
 | 
			
		||||
            icon: markerIcon,
 | 
			
		||||
        });
 | 
			
		||||
        marker.addTo(map);
 | 
			
		||||
    }
 | 
			
		||||
    return [props.address.point.coordinates[1], props.address.point.coordinates[0], 0]
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const makeUrlGoogleMap = (address: Address): string => {
 | 
			
		||||
const map_div = ref<HTMLDivElement | null>(null);
 | 
			
		||||
const map = useLeafletMap(map_div, {zoom: 18, center: latLngMarker});
 | 
			
		||||
const tileLayer = useLeafletTileLayer(
 | 
			
		||||
    "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
 | 
			
		||||
        attribution:
 | 
			
		||||
            '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
useLeafletDisplayLayer(map, tileLayer);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const marker = useLeafletMarker(latLngMarker, {icon: markerIcon});
 | 
			
		||||
useLeafletDisplayLayer(map, marker);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const makeUrlGoogleMap = (address: Address|AddressCreation): string => {
 | 
			
		||||
    const params = new URLSearchParams();
 | 
			
		||||
    params.append("api", "1");
 | 
			
		||||
    if (address.point !== null && address.addressReference !== null) {
 | 
			
		||||
    const point = getAddressPoint(address);
 | 
			
		||||
    if (point !== null && address.addressReference !== null) {
 | 
			
		||||
        params.append(
 | 
			
		||||
            "query",
 | 
			
		||||
            `${address.point.coordinates[1]} ${address.point.coordinates[0]}`,
 | 
			
		||||
            `${point.coordinates[1]} ${point.coordinates[0]}`,
 | 
			
		||||
        );
 | 
			
		||||
    } else {
 | 
			
		||||
        params.append("query", address.lines.join(", "));
 | 
			
		||||
        params.append("query", buildAddressLines(address).join(", "));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return `https://www.google.com/maps/search/?${params.toString()}`;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const makeUrlOsm = (address: Address): string => {
 | 
			
		||||
    if (address.point !== null && address.addressReference !== null) {
 | 
			
		||||
const makeUrlOsm = (address: Address|AddressCreation): string => {
 | 
			
		||||
    const point = getAddressPoint(address);
 | 
			
		||||
    if (point !== null && address.addressReference !== null) {
 | 
			
		||||
        const params = new URLSearchParams();
 | 
			
		||||
        params.append("mlat", `${address.point.coordinates[1]}`);
 | 
			
		||||
        params.append("mlon", `${address.point.coordinates[0]}`);
 | 
			
		||||
        params.append("mlat", `${point.coordinates[1]}`);
 | 
			
		||||
        params.append("mlon", `${point.coordinates[0]}`);
 | 
			
		||||
        const hashParams = new URLSearchParams();
 | 
			
		||||
        hashParams.append(
 | 
			
		||||
            "map",
 | 
			
		||||
            `18/${address.point.coordinates[1]}/${address.point.coordinates[0]}`,
 | 
			
		||||
            `18/${point.coordinates[1]}/${point.coordinates[0]}`,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return `https://www.openstreetmap.org/?${params.toString()}#${hashParams.toString()}`;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const params = new URLSearchParams();
 | 
			
		||||
    params.append("query", address.lines.join(", "));
 | 
			
		||||
    params.append("query", buildAddressLines(address).join(", "));
 | 
			
		||||
 | 
			
		||||
    return `https://www.openstreetmap.org/search?${params.toString()}`;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
import {Address, AddressCreation, Point} from "ChillMainAssets/types";
 | 
			
		||||
import addressFormatter, {Input} from "@fragaria/address-formatter";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if the given object is of type Address by verifying the existence
 | 
			
		||||
 * of the `lines` property and confirming that it is an array of strings.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {AddressCreation | Address} obj - The object to check.
 | 
			
		||||
 * @return {boolean} Returns true if the object is of type Address, otherwise false.
 | 
			
		||||
 */
 | 
			
		||||
export function isAddress(obj: AddressCreation | Address): obj is Address {
 | 
			
		||||
    return (obj as any).lines !== undefined && Array.isArray((obj as any).lines);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function buildAddressFormatterObject(address: AddressCreation): Input {
 | 
			
		||||
    return {
 | 
			
		||||
        city: address.postcode.name,
 | 
			
		||||
        postcode: address.postcode.code,
 | 
			
		||||
        countryCode: address.postcode.country.code,
 | 
			
		||||
        street: address.street,
 | 
			
		||||
        houseNumber: address.streetNumber,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export const buildAddressLines = (address: AddressCreation|Address): string[] => {
 | 
			
		||||
    if (isAddress(address)) {
 | 
			
		||||
        return address.lines;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const lines = addressFormatter.format(buildAddressFormatterObject(address), {output: 'array', countryCode: address.addressReference.postcode.country.code });
 | 
			
		||||
    console.log('lines:', lines);
 | 
			
		||||
    return lines;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const buildAddressText = (address: AddressCreation|Address): string => {
 | 
			
		||||
    return buildAddressLines(address).join(' - ');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const getAddressPoint = (address: AddressCreation|Address): Point|null => {
 | 
			
		||||
    if (isAddress(address)) {
 | 
			
		||||
        return address.point;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return address.addressReference?.point;
 | 
			
		||||
}
 | 
			
		||||
@@ -4,14 +4,14 @@
 | 
			
		||||
            <div v-if="isConfidential">
 | 
			
		||||
                <confidential :position-btn-far="true">
 | 
			
		||||
                    <template #confidential-content>
 | 
			
		||||
                        <div v-if="isMultiline === true">
 | 
			
		||||
                        <div v-if="isMultiline">
 | 
			
		||||
                            <p
 | 
			
		||||
                                v-for="(l, i) in address.lines"
 | 
			
		||||
                                v-for="(l, i) in buildAddressLines(address)"
 | 
			
		||||
                                :key="`line-${i}`"
 | 
			
		||||
                            >
 | 
			
		||||
                                {{ l }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p v-if="showButtonDetails">
 | 
			
		||||
                            <p v-if="showButtonDetails && isAddress(address) ">
 | 
			
		||||
                                <address-details-button
 | 
			
		||||
                                    :address_id="address.address_id"
 | 
			
		||||
                                    :address_ref_status="address.refStatus"
 | 
			
		||||
@@ -19,8 +19,8 @@
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div v-else>
 | 
			
		||||
                            <p v-if="'' !== address.text" class="street">
 | 
			
		||||
                                {{ address.text }}
 | 
			
		||||
                            <p v-if="'' !== buildAddressText(address)" class="street">
 | 
			
		||||
                                {{ buildAddressText(address) }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p
 | 
			
		||||
                                v-if="null !== address.postcode"
 | 
			
		||||
@@ -29,8 +29,8 @@
 | 
			
		||||
                                {{ address.postcode.code }}
 | 
			
		||||
                                {{ address.postcode.name }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                            <p v-if="null !== address.country" class="country">
 | 
			
		||||
                                {{ localizeString(address.country.name) }}
 | 
			
		||||
                            <p v-if="null !== address.postcode" class="country">
 | 
			
		||||
                                {{ localizeString(address.postcode.country.name) }}
 | 
			
		||||
                            </p>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </template>
 | 
			
		||||
@@ -38,11 +38,11 @@
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div v-if="!isConfidential">
 | 
			
		||||
                <div v-if="isMultiline === true">
 | 
			
		||||
                    <p v-for="(l, i) in address.lines" :key="`line-${i}`">
 | 
			
		||||
                <div v-if="isMultiline">
 | 
			
		||||
                    <p v-for="(l, i) in buildAddressLines(address)" :key="`line-${i}`">
 | 
			
		||||
                        {{ l }}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p v-if="showButtonDetails">
 | 
			
		||||
                    <p v-if="showButtonDetails && isAddress(address) ">
 | 
			
		||||
                        <address-details-button
 | 
			
		||||
                            :address_id="address.address_id"
 | 
			
		||||
                            :address_ref_status="address.refStatus"
 | 
			
		||||
@@ -50,9 +50,9 @@
 | 
			
		||||
                    </p>
 | 
			
		||||
                </div>
 | 
			
		||||
                <div v-else>
 | 
			
		||||
                    <p v-if="address.text" class="street">
 | 
			
		||||
                        {{ address.text }}
 | 
			
		||||
                        <template v-if="showButtonDetails">
 | 
			
		||||
                    <p v-if="'' !== buildAddressText(address)" class="street">
 | 
			
		||||
                        {{ buildAddressText(address)}}
 | 
			
		||||
                        <template v-if="showButtonDetails && isAddress(address) ">
 | 
			
		||||
                            <address-details-button
 | 
			
		||||
                                :address_id="address.address_id"
 | 
			
		||||
                                :address_ref_status="address.refStatus"
 | 
			
		||||
@@ -63,68 +63,49 @@
 | 
			
		||||
            </div>
 | 
			
		||||
        </component>
 | 
			
		||||
 | 
			
		||||
        <div v-if="useDatePane === true" class="address-more">
 | 
			
		||||
        <div v-if="useDatePane" class="address-more">
 | 
			
		||||
            <div v-if="address.validFrom">
 | 
			
		||||
                <span class="validFrom">
 | 
			
		||||
                    <b>{{ trans(ADDRESS_VALID_FROM) }}</b
 | 
			
		||||
                    >: {{ $d(address.validFrom.date) }}
 | 
			
		||||
                    >: {{ address.validFrom?.datetime8601 }}
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div v-if="address.validTo">
 | 
			
		||||
            <div v-if="isAddress(address) && address.validTo !== null">
 | 
			
		||||
                <span class="validTo">
 | 
			
		||||
                    <b>{{ trans(ADDRESS_VALID_TO) }}</b
 | 
			
		||||
                    >: {{ $d(address.validTo.date) }}
 | 
			
		||||
                    >: {{ address.validTo?.datetime8601 }}
 | 
			
		||||
                </span>
 | 
			
		||||
            </div>
 | 
			
		||||
        </div>
 | 
			
		||||
    </component>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script>
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { computed } from "vue";
 | 
			
		||||
import Confidential from "ChillMainAssets/vuejs/_components/Confidential.vue";
 | 
			
		||||
import AddressDetailsButton from "ChillMainAssets/vuejs/_components/AddressDetails/AddressDetailsButton.vue";
 | 
			
		||||
import { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO } from "translator";
 | 
			
		||||
import { localizeString } from "ChillMainAssets/lib/localizationHelper/localizationHelper";
 | 
			
		||||
import {Address, AddressCreation} from "ChillMainAssets/types";
 | 
			
		||||
import {isAddress, buildAddressLines, buildAddressText} from "ChillMainAssets/vuejs/_components/AddressDetails/helper";
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
    name: "AddressRenderBox",
 | 
			
		||||
    methods: { localizeString },
 | 
			
		||||
    components: {
 | 
			
		||||
        Confidential,
 | 
			
		||||
        AddressDetailsButton,
 | 
			
		||||
    },
 | 
			
		||||
    props: {
 | 
			
		||||
        address: {
 | 
			
		||||
            type: Object,
 | 
			
		||||
        },
 | 
			
		||||
        isMultiline: {
 | 
			
		||||
            default: true,
 | 
			
		||||
            type: Boolean,
 | 
			
		||||
        },
 | 
			
		||||
        useDatePane: {
 | 
			
		||||
            default: false,
 | 
			
		||||
            type: Boolean,
 | 
			
		||||
        },
 | 
			
		||||
        showButtonDetails: {
 | 
			
		||||
            default: true,
 | 
			
		||||
            type: Boolean,
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    setup() {
 | 
			
		||||
        return { trans, ADDRESS_VALID_FROM, ADDRESS_VALID_TO };
 | 
			
		||||
    },
 | 
			
		||||
    computed: {
 | 
			
		||||
        component() {
 | 
			
		||||
            return this.isMultiline === true ? "div" : "span";
 | 
			
		||||
        },
 | 
			
		||||
        multiline() {
 | 
			
		||||
            return this.isMultiline === true ? "multiline" : "";
 | 
			
		||||
        },
 | 
			
		||||
        isConfidential() {
 | 
			
		||||
            return this.address.confidential;
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
};
 | 
			
		||||
const props = withDefaults(
 | 
			
		||||
    defineProps<{
 | 
			
		||||
        address: Address|AddressCreation;
 | 
			
		||||
        isMultiline?: boolean;
 | 
			
		||||
        useDatePane?: boolean;
 | 
			
		||||
        showButtonDetails?: boolean;
 | 
			
		||||
    }>(),
 | 
			
		||||
    {
 | 
			
		||||
        isMultiline: true,
 | 
			
		||||
        useDatePane: false,
 | 
			
		||||
        showButtonDetails: true,
 | 
			
		||||
    }
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
const component = computed(() => (props.isMultiline ? "div" : "span"));
 | 
			
		||||
const multiline = computed(() => (props.isMultiline ? "multiline" : ""));
 | 
			
		||||
const isConfidential = computed(() => Boolean(props.address?.confidential));
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" scoped>
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
{% extends '@ChillMain/layout.html.twig' %}
 | 
			
		||||
 | 
			
		||||
{% block css %}
 | 
			
		||||
    {{ encore_entry_link_tags('address_picker') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block js %}
 | 
			
		||||
    {{ encore_entry_script_tags('address_picker') }}
 | 
			
		||||
{% endblock %}
 | 
			
		||||
 | 
			
		||||
{% block content %}
 | 
			
		||||
 | 
			
		||||
    <div data-address-picker="data-address-picker"></div>
 | 
			
		||||
 | 
			
		||||
{% endblock %}
 | 
			
		||||
@@ -66,6 +66,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
 | 
			
		||||
                    'name' => $address->getPostCode()->getName(),
 | 
			
		||||
                    'code' => $address->getPostCode()->getCode(),
 | 
			
		||||
                    'center' => $address->getPostcode()->getCenter(),
 | 
			
		||||
                    'country' => $this->normalizer->normalize($address->getPostCode()->getCountry(), $format, [AbstractNormalizer::GROUPS => ['read']]),
 | 
			
		||||
                ],
 | 
			
		||||
                'country' => [
 | 
			
		||||
                    'id' => $address->getPostCode()->getCountry()->getId(),
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,118 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Tests\Controller;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Controller\AddressReferenceAggregatedApiController;
 | 
			
		||||
use Chill\MainBundle\Repository\AddressReferenceRepositoryInterface;
 | 
			
		||||
use PHPUnit\Framework\TestCase;
 | 
			
		||||
use Prophecy\PhpUnit\ProphecyTrait;
 | 
			
		||||
use Symfony\Component\HttpFoundation\Request;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 | 
			
		||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
 | 
			
		||||
use Symfony\Component\Security\Core\Security;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @covers \Chill\MainBundle\Controller\AddressReferenceAggregatedApiController
 | 
			
		||||
 */
 | 
			
		||||
final class AddressReferenceAggregatedApiControllerTest extends TestCase
 | 
			
		||||
{
 | 
			
		||||
    use ProphecyTrait;
 | 
			
		||||
 | 
			
		||||
    public function testAccessDeniedWhenNotAuthenticated(): void
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('IS_AUTHENTICATED')->willReturn(false);
 | 
			
		||||
 | 
			
		||||
        $repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
 | 
			
		||||
 | 
			
		||||
        $controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
 | 
			
		||||
 | 
			
		||||
        $request = new Request(query: ['q' => 'anything']);
 | 
			
		||||
 | 
			
		||||
        $this->expectException(AccessDeniedHttpException::class);
 | 
			
		||||
 | 
			
		||||
        $controller->search($request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testBadRequestWhenQueryIsMissing(): void
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('IS_AUTHENTICATED')->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
 | 
			
		||||
 | 
			
		||||
        $controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
 | 
			
		||||
 | 
			
		||||
        $request = new Request();
 | 
			
		||||
 | 
			
		||||
        $this->expectException(BadRequestHttpException::class);
 | 
			
		||||
        $this->expectExceptionMessage('Parameter "q" is required.');
 | 
			
		||||
 | 
			
		||||
        $controller->search($request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testBadRequestWhenQueryIsEmpty(): void
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('IS_AUTHENTICATED')->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
 | 
			
		||||
 | 
			
		||||
        $controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
 | 
			
		||||
 | 
			
		||||
        $request = new Request(query: ['q' => '   ']);
 | 
			
		||||
 | 
			
		||||
        $this->expectException(BadRequestHttpException::class);
 | 
			
		||||
        $this->expectExceptionMessage('Parameter "q" is required and cannot be empty.');
 | 
			
		||||
 | 
			
		||||
        $controller->search($request);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function testSuccessfulSearchReturnsJsonAndCallsRepositoryWithTrimmedQuery(): void
 | 
			
		||||
    {
 | 
			
		||||
        $security = $this->prophesize(Security::class);
 | 
			
		||||
        $security->isGranted('IS_AUTHENTICATED')->willReturn(true);
 | 
			
		||||
 | 
			
		||||
        $expectedQuery = 'foo';
 | 
			
		||||
        $iterableResult = new \ArrayIterator([
 | 
			
		||||
            [
 | 
			
		||||
                'street' => 'Main Street',
 | 
			
		||||
                'postcode_id' => 123,
 | 
			
		||||
                'code' => '1000',
 | 
			
		||||
                'label' => 'Brussels',
 | 
			
		||||
                'positions' => ['1' => '12', '2' => '14'],
 | 
			
		||||
                'row_number' => 1,
 | 
			
		||||
            ],
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        $repo = $this->prophesize(AddressReferenceRepositoryInterface::class);
 | 
			
		||||
        $repo->findAggregatedBySearchString($expectedQuery)->willReturn($iterableResult)->shouldBeCalledOnce();
 | 
			
		||||
 | 
			
		||||
        $controller = new AddressReferenceAggregatedApiController($security->reveal(), $repo->reveal());
 | 
			
		||||
 | 
			
		||||
        // Provide spaces around to ensure trimming is applied
 | 
			
		||||
        $request = new Request(query: ['q' => "  {$expectedQuery}  "]);
 | 
			
		||||
 | 
			
		||||
        $response = $controller->search($request);
 | 
			
		||||
 | 
			
		||||
        self::assertSame(200, $response->getStatusCode());
 | 
			
		||||
        $data = json_decode($response->getContent(), true, 512, JSON_THROW_ON_ERROR);
 | 
			
		||||
        self::assertIsArray($data);
 | 
			
		||||
        self::assertCount(1, $data);
 | 
			
		||||
        self::assertSame('Main Street', $data[0]['street']);
 | 
			
		||||
        self::assertSame(123, $data[0]['postcode_id']);
 | 
			
		||||
        self::assertSame('1000', $data[0]['code']);
 | 
			
		||||
        self::assertSame('Brussels', $data[0]['label']);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,88 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Tests\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Entity\AddressReference;
 | 
			
		||||
use Chill\MainBundle\Entity\PostalCode;
 | 
			
		||||
use Chill\MainBundle\Repository\AddressReferenceRepository;
 | 
			
		||||
use Doctrine\ORM\EntityManagerInterface;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
class AddressReferenceRepositoryTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    private static AddressReferenceRepository $repository;
 | 
			
		||||
 | 
			
		||||
    public static function setUpBeforeClass(): void
 | 
			
		||||
    {
 | 
			
		||||
        static::bootKernel();
 | 
			
		||||
        static::$repository = static::getContainer()->get(AddressReferenceRepository::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateSearchString
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
 | 
			
		||||
    {
 | 
			
		||||
        $actual = static::$repository->findBySearchString($search, $postalCode);
 | 
			
		||||
 | 
			
		||||
        self::assertIsIterable($actual, $text);
 | 
			
		||||
 | 
			
		||||
        if (null !== $expected) {
 | 
			
		||||
            self::assertEquals($expected, iterator_to_array($actual));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateSearchString
 | 
			
		||||
     */
 | 
			
		||||
    public function testCountBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
 | 
			
		||||
    {
 | 
			
		||||
        $actual = static::$repository->countBySearchString($search, $postalCode);
 | 
			
		||||
 | 
			
		||||
        self::assertIsInt($actual, $text);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @dataProvider generateSearchString
 | 
			
		||||
     */
 | 
			
		||||
    public function testFindAggreggateBySearchString(string $search, int|PostalCode|null $postalCode, string $text, ?array $expected = null): void
 | 
			
		||||
    {
 | 
			
		||||
        $actual = static::$repository->findAggregatedBySearchString($search, $postalCode);
 | 
			
		||||
 | 
			
		||||
        self::assertIsIterable($actual, $text);
 | 
			
		||||
 | 
			
		||||
        if (null !== $expected) {
 | 
			
		||||
            self::assertEquals($expected, iterator_to_array($actual));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static function generateSearchString(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $em = static::getContainer()->get(EntityManagerInterface::class);
 | 
			
		||||
        /** @var AddressReference $ar */
 | 
			
		||||
        $ar = $em->createQuery('SELECT ar FROM '.AddressReference::class.' ar')
 | 
			
		||||
            ->setMaxResults(1)
 | 
			
		||||
            ->getSingleResult();
 | 
			
		||||
 | 
			
		||||
        yield ['', null, 'search with empty string', []];
 | 
			
		||||
        yield ['   ', null, 'search with spaces only', []];
 | 
			
		||||
        yield ['rue des    moulins', null, 'search contains an empty string'];
 | 
			
		||||
        yield ['rue des moulins', $ar->getPostcode()->getId(), 'search with postal code as an id'];
 | 
			
		||||
        yield ['rue des moulins', $ar->getPostcode(), 'search with postal code instance'];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,61 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\MainBundle\Tests\Repository;
 | 
			
		||||
 | 
			
		||||
use Chill\MainBundle\Repository\PostalCodeForAddressReferenceRepository;
 | 
			
		||||
use Doctrine\DBAL\Connection;
 | 
			
		||||
use PHPUnit\Framework\Attributes\DataProvider;
 | 
			
		||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @internal
 | 
			
		||||
 *
 | 
			
		||||
 * @coversNothing
 | 
			
		||||
 */
 | 
			
		||||
final class PostalCodeForAddressReferenceRepositoryTest extends KernelTestCase
 | 
			
		||||
{
 | 
			
		||||
    private Connection $connection;
 | 
			
		||||
 | 
			
		||||
    protected function setUp(): void
 | 
			
		||||
    {
 | 
			
		||||
        self::bootKernel();
 | 
			
		||||
        $this->connection = self::getContainer()->get(Connection::class);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * @return iterable<string[]>
 | 
			
		||||
     */
 | 
			
		||||
    public static function provideSearches(): iterable
 | 
			
		||||
    {
 | 
			
		||||
        yield [''];
 | 
			
		||||
        yield ['   '];
 | 
			
		||||
        yield ['hugo'];
 | 
			
		||||
        yield ['   hugo'];
 | 
			
		||||
        yield ['hugo   '];
 | 
			
		||||
        yield ['rue victor hugo'];
 | 
			
		||||
        yield ['rue   victor hugo'];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #[DataProvider('provideSearches')]
 | 
			
		||||
    public function testFindPostalCodeDoesNotErrorAndIsIterable(string $search): void
 | 
			
		||||
    {
 | 
			
		||||
        $repository = new PostalCodeForAddressReferenceRepository($this->connection);
 | 
			
		||||
 | 
			
		||||
        $result = $repository->findPostalCode($search);
 | 
			
		||||
 | 
			
		||||
        self::assertIsIterable($result);
 | 
			
		||||
 | 
			
		||||
        // Ensure it can be converted to an array (and iterate without error)
 | 
			
		||||
        $rows = \is_array($result) ? $result : iterator_to_array($result, false);
 | 
			
		||||
        self::assertIsArray($rows);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -595,6 +595,44 @@ paths:
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
 | 
			
		||||
    /1.0/main/address-reference/aggregated/search:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - address
 | 
			
		||||
            summary: Search for address reference aggregated
 | 
			
		||||
            parameters:
 | 
			
		||||
                - name: q
 | 
			
		||||
                  in: query
 | 
			
		||||
                  required: true
 | 
			
		||||
                  description: The search pattern
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
                400:
 | 
			
		||||
                    description: "Bad Request"
 | 
			
		||||
    /1.0/main/address-reference/postal-code/search:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
                - address
 | 
			
		||||
            summary: Search for postal code that can contains the search query
 | 
			
		||||
            parameters:
 | 
			
		||||
                - name: q
 | 
			
		||||
                  in: query
 | 
			
		||||
                  required: true
 | 
			
		||||
                  description: The search pattern
 | 
			
		||||
                  schema:
 | 
			
		||||
                      type: string
 | 
			
		||||
            responses:
 | 
			
		||||
                200:
 | 
			
		||||
                    description: "ok"
 | 
			
		||||
                401:
 | 
			
		||||
                    description: "Unauthorized"
 | 
			
		||||
                400:
 | 
			
		||||
                    description: "Bad Request"
 | 
			
		||||
    /1.0/main/postal-code/search.json:
 | 
			
		||||
        get:
 | 
			
		||||
            tags:
 | 
			
		||||
 
 | 
			
		||||
@@ -120,6 +120,11 @@ module.exports = function (encore, entries) {
 | 
			
		||||
    "vue_onthefly",
 | 
			
		||||
    __dirname + "/Resources/public/vuejs/OnTheFly/index.js",
 | 
			
		||||
  );
 | 
			
		||||
  encore.addEntry(
 | 
			
		||||
    'address_picker',
 | 
			
		||||
    __dirname + "/Resources/public/vuejs/AddressPicker/index.ts",
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  encore.addEntry(
 | 
			
		||||
    "page_workflow_waiting_post_process",
 | 
			
		||||
    __dirname + "/Resources/public/vuejs/WaitPostProcessWorkflow/index.ts"
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,52 @@
 | 
			
		||||
<?php
 | 
			
		||||
 | 
			
		||||
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 Chill\Migrations\Main;
 | 
			
		||||
 | 
			
		||||
use Doctrine\DBAL\Schema\Schema;
 | 
			
		||||
use Doctrine\Migrations\AbstractMigration;
 | 
			
		||||
 | 
			
		||||
final class Version20250214154310 extends AbstractMigration
 | 
			
		||||
{
 | 
			
		||||
    public function getDescription(): string
 | 
			
		||||
    {
 | 
			
		||||
        return 'Create view for searching address reference';
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function up(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            create materialized view public.view_chill_main_address_reference as
 | 
			
		||||
            SELECT row_number() OVER ()       AS row_number,
 | 
			
		||||
                   cmar.street                AS street,
 | 
			
		||||
                   cmar.streetnumber          AS streetnumber,
 | 
			
		||||
                   cmar.id                    AS address_id,
 | 
			
		||||
                   lower(unaccent(
 | 
			
		||||
                           (((((cmar.street || ' '::text) || cmar.streetnumber) || ' '::text) || cmpc.code::text) || ' '::text) ||
 | 
			
		||||
                           cmpc.label::text)) AS address,
 | 
			
		||||
                   cmpc.id                    AS postcode_id
 | 
			
		||||
            FROM chill_main_address_reference cmar
 | 
			
		||||
                     JOIN chill_main_postal_code cmpc ON cmar.postcode_id = cmpc.id
 | 
			
		||||
            WHERE cmar.deletedat IS NULL
 | 
			
		||||
            ORDER BY ((cmpc.code::text || ' '::text) || cmpc.label::text), cmar.street, (lpad(cmar.streetnumber, 10, '0'::text));
 | 
			
		||||
            SQL);
 | 
			
		||||
 | 
			
		||||
        $this->addSql(<<<'SQL'
 | 
			
		||||
            create index if not exists view_chill_internal_address_reference_trgm
 | 
			
		||||
                on view_chill_main_address_reference using gist (postcode_id, address public.gist_trgm_ops);
 | 
			
		||||
        SQL);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public function down(Schema $schema): void
 | 
			
		||||
    {
 | 
			
		||||
        $this->addSql('DROP MATERIALIZED VIEW view_chill_main_address_reference');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -181,6 +181,11 @@ address more:
 | 
			
		||||
    buildingName: résidence
 | 
			
		||||
    extra: ""
 | 
			
		||||
    distribution: cedex
 | 
			
		||||
 | 
			
		||||
address_picker:
 | 
			
		||||
    # placeholder
 | 
			
		||||
    Search for addresses: Chercher des adresses
 | 
			
		||||
 | 
			
		||||
Create a new address: Créer une nouvelle adresse
 | 
			
		||||
Create an address: Créer une adresse
 | 
			
		||||
Update address: Modifier l'adresse
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user