diff --git a/package.json b/package.json index 0bd3a81e5..590976cfa 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@fullcalendar/timegrid": "^6.1.4", "@fullcalendar/vue3": "^6.1.4", "@popperjs/core": "^2.9.2", + "@types/leaflet": "^1.9.3", "dropzone": "^5.7.6", "es6-promise": "^4.2.8", "leaflet": "^1.7.1", diff --git a/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php b/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php new file mode 100644 index 000000000..5cdefceb5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/AddressToReferenceMatcherController.php @@ -0,0 +1,111 @@ +security = $security; + $this->entityManager = $entityManager; + $this->serializer = $serializer; + } + + /** + * @Route("/api/1.0/main/address/reference-match/{id}/set/reviewed", methods={"POST"}) + */ + public function markAddressAsReviewed(Address $address): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + $address->setRefStatus(Address::ADDR_REFERENCE_STATUS_REVIEWED); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } + + /** + * Set an address back to "to review". Only if the address is in "reviewed" state. + * + * @Route("/api/1.0/main/address/reference-match/{id}/set/to_review", methods={"POST"}) + */ + public function markAddressAsToReview(Address $address): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + if (Address::ADDR_REFERENCE_STATUS_REVIEWED !== $address->getRefStatus()) { + throw new AccessDeniedHttpException("forbidden to mark a matching address to 'to review'"); + } + + $address->setRefStatus(Address::ADDR_REFERENCE_STATUS_TO_REVIEW); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } + + /** + * @Route("/api/1.0/main/address/reference-match/{id}/sync-with-reference", methods={"POST"}) + */ + public function syncAddressWithReference(Address $address): JsonResponse + { + if (null === $address->getAddressReference()) { + throw new BadRequestHttpException('this address does not have any address reference'); + } + + $address->syncWithReference($address->getAddressReference()); + + $this->entityManager->flush(); + + return new JsonResponse( + $this->serializer->serialize($address, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } + +} diff --git a/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php b/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php new file mode 100644 index 000000000..988439de5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/GeographicalUnitByAddressApiController.php @@ -0,0 +1,75 @@ +paginatorFactory = $paginatorFactory; + $this->geographicalUnitRepository = $geographicalUnitRepository; + $this->security = $security; + $this->serializer = $serializer; + } + + /** + * @Route("/api/1.0/main/geographical-unit/by-address/{id}.{_format}", requirements={"_format": "json"}) + */ + public function getGeographicalUnitCoveringAddress(Address $address): JsonResponse + { + if (!$this->security->isGranted('ROLE_USER')) { + throw new AccessDeniedHttpException(); + } + + $count = $this->geographicalUnitRepository->countGeographicalUnitContainingAddress($address); + $pagination = $this->paginatorFactory->create($count); + $units = $this->geographicalUnitRepository->findGeographicalUnitContainingAddress($address, $pagination->getCurrentPageFirstItemNumber(), $pagination->getItemsPerPage()); + + $collection = new Collection($units, $pagination); + + return new JsonResponse( + $this->serializer->serialize($collection, 'json', [AbstractNormalizer::GROUPS => ['read']]), + JsonResponse::HTTP_OK, + [], + true + ); + } +} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 69546016a..c86a69be2 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -46,6 +46,7 @@ use Chill\MainBundle\Doctrine\Type\NativeDateIntervalType; use Chill\MainBundle\Doctrine\Type\PointType; use Chill\MainBundle\Entity\Civility; use Chill\MainBundle\Entity\Country; +use Chill\MainBundle\Entity\GeographicalUnitLayer; use Chill\MainBundle\Entity\Language; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\LocationType; @@ -732,6 +733,20 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => GeographicalUnitLayer::class, + 'name' => 'geographical-unit-layer', + 'base_path' => '/api/1.0/main/geographical-unit-layer', + 'base_role' => 'ROLE_USER', + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + ], + ] ], ]); } diff --git a/src/Bundle/ChillMainBundle/Entity/Address.php b/src/Bundle/ChillMainBundle/Entity/Address.php index 9a0f8b7b3..8e5299579 100644 --- a/src/Bundle/ChillMainBundle/Entity/Address.php +++ b/src/Bundle/ChillMainBundle/Entity/Address.php @@ -12,6 +12,10 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity; use Chill\MainBundle\Doctrine\Model\Point; +use Chill\MainBundle\Doctrine\Model\TrackCreationInterface; +use Chill\MainBundle\Doctrine\Model\TrackCreationTrait; +use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface; +use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait; use Chill\ThirdPartyBundle\Entity\ThirdParty; use DateTime; use DateTimeInterface; @@ -28,8 +32,28 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface; * @ORM\Table(name="chill_main_address") * @ORM\HasLifecycleCallbacks */ -class Address +class Address implements TrackCreationInterface, TrackUpdateInterface { + use TrackCreationTrait; + use TrackUpdateTrait; + + /** + * When an Address does match with the AddressReference + */ + public const ADDR_REFERENCE_STATUS_MATCH = 'match'; + + /** + * When an Address does not match with the AddressReference, and + * is pending for a review + */ + public const ADDR_REFERENCE_STATUS_TO_REVIEW = 'to_review'; + + /** + * When an Address does not match with the AddressReference, but + * is reviewed + */ + public const ADDR_REFERENCE_STATUS_REVIEWED = 'reviewed'; + /** * @ORM\ManyToOne(targetEntity=AddressReference::class) * @Groups({"write"}) @@ -37,67 +61,48 @@ class Address private ?AddressReference $addressReference = null; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $buildingName; + private string $buildingName = ''; /** - * @ORM\Column(type="boolean") + * @ORM\Column(type="boolean", options={"default": false}) * @Groups({"write"}) */ private bool $confidential = false; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $corridor; + private string $corridor = ''; /** - * A list of metadata, added by customizable fields. - * - * @var array - */ - private $customs = []; - - /** - * @var string|null - * * used for the CEDEX information * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $distribution; + private string $distribution = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $extra; + private string $extra = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $flat; + private string $flat = ''; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $floor; + private string $floor = ''; /** * List of geographical units and addresses. @@ -131,11 +136,9 @@ class Address * True if the address is a "no address", aka homeless person, ... * * @Groups({"write"}) - * @ORM\Column(type="boolean") - * - * @var bool + * @ORM\Column(type="boolean", options={"default": false}) */ - private $isNoAddress = false; + private bool $isNoAddress = false; /** * A ThirdParty reference for person's addresses that are linked to a third party. @@ -146,7 +149,7 @@ class Address * @Groups({"write"}) * @ORM\JoinColumn(nullable=true, onDelete="SET NULL") */ - private $linkedToThirdParty; + private ?ThirdParty $linkedToThirdParty; /** * A geospatial field storing the coordinates of the Address. @@ -156,7 +159,7 @@ class Address * @ORM\Column(type="point", nullable=true) * @Groups({"write"}) */ - private $point; + private ?Point $point = null; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") @@ -166,28 +169,36 @@ class Address private ?PostalCode $postcode = null; /** - * @var string|null - * - * @ORM\Column(type="string", length=255, nullable=true) - * @Groups({"write"}) + * @var self::ADDR_REFERENCE_STATUS_* + * @ORM\Column(type="text", nullable=false, options={"default": self::ADDR_REFERENCE_STATUS_MATCH}) */ - private $steps; + private string $refStatus = self::ADDR_REFERENCE_STATUS_MATCH; /** - * @var string - * - * @ORM\Column(type="string", length=255) - * @Groups({"write"}) + * @ORM\Column(type="datetime_immutable", nullable=false, options={"default": "CURRENT_TIMESTAMP"}) */ - private $street = ''; + private \DateTimeImmutable $refStatusLastUpdate; /** - * @var string * - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @Groups({"write"}) */ - private $streetNumber = ''; + private string $steps = ''; + + /** + * + * @ORM\Column(type="text", nullable=false, options={"default": ""}) + * @Groups({"write"}) + */ + private string $street = ''; + + /** + * + * @ORM\Column(type="text", nullable=false, options={"default": ""}) + * @Groups({"write"}) + */ + private string $streetNumber = ''; /** * Indicates when the address starts validation. Used to build an history @@ -210,6 +221,7 @@ class Address public function __construct() { $this->validFrom = new DateTime(); + $this->refStatusLastUpdate = new \DateTimeImmutable('now'); $this->geographicalUnits = new ArrayCollection(); } @@ -220,7 +232,6 @@ class Address ->setBuildingName($original->getBuildingName()) ->setConfidential($original->getConfidential()) ->setCorridor($original->getCorridor()) - ->setCustoms($original->getCustoms()) ->setDistribution($original->getDistribution()) ->setExtra($original->getExtra()) ->setFlat($original->getFlat()) @@ -239,11 +250,20 @@ class Address public static function createFromAddressReference(AddressReference $original): Address { return (new Address()) - ->setPoint($original->getPoint()) - ->setPostcode($original->getPostcode()) - ->setStreet($original->getStreet()) - ->setStreetNumber($original->getStreetNumber()) - ->setAddressReference($original); + ->syncWithReference($original); + } + + public function syncWithReference(AddressReference $addressReference): Address + { + $this + ->setPoint($addressReference->getPoint()) + ->setPostcode($addressReference->getPostcode()) + ->setStreet($addressReference->getStreet()) + ->setStreetNumber($addressReference->getStreetNumber()) + ->setRefStatus(self::ADDR_REFERENCE_STATUS_MATCH) + ->setAddressReference($addressReference); + + return $this; } public function getAddressReference(): ?AddressReference @@ -251,7 +271,7 @@ class Address return $this->addressReference; } - public function getBuildingName(): ?string + public function getBuildingName(): string { return $this->buildingName; } @@ -261,35 +281,27 @@ class Address return $this->confidential; } - public function getCorridor(): ?string + public function getCorridor(): string { return $this->corridor; } - /** - * Get customs informations in the address. - */ - public function getCustoms(): array - { - return $this->customs; - } - - public function getDistribution(): ?string + public function getDistribution(): string { return $this->distribution; } - public function getExtra(): ?string + public function getExtra(): string { return $this->extra; } - public function getFlat(): ?string + public function getFlat(): string { return $this->flat; } - public function getFloor(): ?string + public function getFloor(): string { return $this->floor; } @@ -340,12 +352,22 @@ class Address return $this->postcode; } - public function getSteps(): ?string + public function getRefStatus(): string + { + return $this->refStatus; + } + + public function getRefStatusLastUpdate(): \DateTimeImmutable + { + return $this->refStatusLastUpdate; + } + + public function getSteps(): string { return $this->steps; } - public function getStreet(): ?string + public function getStreet(): string { return $this->street; } @@ -354,6 +376,7 @@ class Address * Get streetAddress1 (legacy function). * * @return string + * @deprecated */ public function getStreetAddress1() { @@ -364,13 +387,14 @@ class Address * Get streetAddress2 (legacy function). * * @return string + * @deprecated */ public function getStreetAddress2() { return $this->streetNumber; } - public function getStreetNumber(): ?string + public function getStreetNumber(): string { return $this->streetNumber; } @@ -378,7 +402,7 @@ class Address /** * @return DateTime */ - public function getValidFrom() + public function getValidFrom(): DateTime { return $this->validFrom; } @@ -407,7 +431,7 @@ class Address public function setBuildingName(?string $buildingName): self { - $this->buildingName = $buildingName; + $this->buildingName = (string) $buildingName; return $this; } @@ -421,47 +445,35 @@ class Address public function setCorridor(?string $corridor): self { - $this->corridor = $corridor; - - return $this; - } - - /** - * Store custom informations in the address. - * - * @return $this - */ - public function setCustoms(array $customs): self - { - $this->customs = $customs; + $this->corridor = (string) $corridor; return $this; } public function setDistribution(?string $distribution): self { - $this->distribution = $distribution; + $this->distribution = (string) $distribution; return $this; } public function setExtra(?string $extra): self { - $this->extra = $extra; + $this->extra = (string) $extra; return $this; } public function setFlat(?string $flat): self { - $this->flat = $flat; + $this->flat = (string) $flat; return $this; } public function setFloor(?string $floor): self { - $this->floor = $floor; + $this->floor = (string) $floor; return $this; } @@ -508,19 +520,44 @@ class Address return $this; } + /** + * Update the ref status + * + * <<<<<<< HEAD + * @param Address::ADDR_REFERENCE_STATUS_* $refStatus + * @param bool|null $updateLastUpdate Also update the "refStatusLastUpdate" + * ======= + * The refstatuslast update is also updated + * >>>>>>> 31152616d (Feature: Provide api endpoint for reviewing addresses) + */ + public function setRefStatus(string $refStatus, ?bool $updateLastUpdate = true): self + { + $this->refStatus = $refStatus; + + if ($updateLastUpdate) { + $this->setRefStatusLastUpdate(new \DateTimeImmutable('now')); + } + + return $this; + } + + public function setRefStatusLastUpdate(\DateTimeImmutable $refStatusLastUpdate): self + { + $this->refStatusLastUpdate = $refStatusLastUpdate; + + return $this; + } + public function setSteps(?string $steps): self { - $this->steps = $steps; + $this->steps = (string) $steps; return $this; } public function setStreet(?string $street): self { - if (null === $street) { - $street = ''; - } - $this->street = $street; + $this->street = (string) $street; return $this; } @@ -531,6 +568,7 @@ class Address * @param string $streetAddress1 * * @return Address + * @deprecated */ public function setStreetAddress1($streetAddress1) { @@ -543,7 +581,7 @@ class Address * Set streetAddress2 (legacy function). * * @param string $streetAddress2 - * + * @deprecated * @return Address */ public function setStreetAddress2($streetAddress2) @@ -555,10 +593,7 @@ class Address public function setStreetNumber(?string $streetNumber): self { - if (null === $streetNumber) { - $streetNumber = ''; - } - $this->streetNumber = $streetNumber; + $this->streetNumber = (string) $streetNumber; return $this; } @@ -605,7 +640,7 @@ class Address return; } - if (empty($this->getStreetAddress1())) { + if ('' === $this->getStreet()) { $context ->buildViolation('address.street1-should-be-set') ->atPath('streetAddress1') diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index 68cc32186..57421749f 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -55,13 +55,13 @@ class AddressReference * @ORM\Column(type="integer") * @groups({"read"}) */ - private $id; + private ?int $id; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $municipalityCode; + private string $municipalityCode = ''; /** * A geospatial field storing the coordinates of the Address. @@ -71,7 +71,7 @@ class AddressReference * @ORM\Column(type="point") * @groups({"read"}) */ - private $point; + private ?Point $point = null; /** * @var PostalCode @@ -79,31 +79,31 @@ class AddressReference * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\PostalCode") * @groups({"read"}) */ - private $postcode; + private ?PostalCode $postcode; /** - * @ORM\Column(type="string", length=255) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $refId; + private string $refId = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $source; + private string $source = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $street; + private string $street = ''; /** - * @ORM\Column(type="string", length=255, nullable=true) + * @ORM\Column(type="text", nullable=false, options={"default": ""}) * @groups({"read"}) */ - private $streetNumber; + private string $streetNumber = ''; /** * @ORM\Column(type="datetime_immutable", nullable=true) @@ -126,7 +126,7 @@ class AddressReference return $this->id; } - public function getMunicipalityCode(): ?string + public function getMunicipalityCode(): string { return $this->municipalityCode; } @@ -141,27 +141,27 @@ class AddressReference * * @return PostalCode */ - public function getPostcode() + public function getPostcode(): ?PostalCode { return $this->postcode; } - public function getRefId(): ?string + public function getRefId(): string { return $this->refId; } - public function getSource(): ?string + public function getSource(): string { return $this->source; } - public function getStreet(): ?string + public function getStreet(): string { return $this->street; } - public function getStreetNumber(): ?string + public function getStreetNumber(): string { return $this->streetNumber; } @@ -192,7 +192,7 @@ class AddressReference public function setMunicipalityCode(?string $municipalityCode): self { - $this->municipalityCode = $municipalityCode; + $this->municipalityCode = (string) $municipalityCode; return $this; } @@ -227,21 +227,21 @@ class AddressReference public function setSource(?string $source): self { - $this->source = $source; + $this->source = (string) $source; return $this; } public function setStreet(?string $street): self { - $this->street = $street; + $this->street = (string) $street; return $this; } public function setStreetNumber(?string $streetNumber): self { - $this->streetNumber = $streetNumber; + $this->streetNumber = (string) $streetNumber; return $this; } diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php index 34f16a0fb..7add53066 100644 --- a/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnit/SimpleGeographicalUnitDTO.php @@ -11,6 +11,8 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity\GeographicalUnit; +use Symfony\Component\Serializer\Annotation as Serializer; + /** * Simple GeographialUnit Data Transfer Object. * @@ -21,24 +23,28 @@ class SimpleGeographicalUnitDTO /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public int $id; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public int $layerId; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public string $unitName; /** * @readonly * @psalm-readonly + * @Serializer\Groups({"read"}) */ public string $unitRefId; diff --git a/src/Bundle/ChillMainBundle/Entity/GeographicalUnitLayer.php b/src/Bundle/ChillMainBundle/Entity/GeographicalUnitLayer.php index bdea40563..b856c7a21 100644 --- a/src/Bundle/ChillMainBundle/Entity/GeographicalUnitLayer.php +++ b/src/Bundle/ChillMainBundle/Entity/GeographicalUnitLayer.php @@ -14,6 +14,7 @@ namespace Chill\MainBundle\Entity; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Table(name="chill_main_geographical_unit_layer", uniqueConstraints={ @@ -27,16 +28,19 @@ class GeographicalUnitLayer * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") + * @Serializer\Groups({"read"}) */ private ?int $id = null; /** * @ORM\Column(type="json", nullable=false, options={"default": "[]"}) + * @Serializer\Groups({"read"}) */ private array $name = []; /** * @ORM\Column(type="text", nullable=false, options={"default": ""}) + * @Serializer\Groups({"read"}) */ private string $refId = ''; diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/AddressDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/AddressDataMapper.php index 8961627d4..ca9c8452e 100644 --- a/src/Bundle/ChillMainBundle/Form/DataMapper/AddressDataMapper.php +++ b/src/Bundle/ChillMainBundle/Form/DataMapper/AddressDataMapper.php @@ -43,11 +43,13 @@ class AddressDataMapper implements DataMapperInterface /** @var FormInterface $form */ switch ($key) { case 'streetAddress1': + /** @phpstan-ignore-next-line */ $form->setData($address->getStreetAddress1()); break; case 'streetAddress2': + /** @phpstan-ignore-next-line */ $form->setData($address->getStreetAddress2()); break; @@ -110,11 +112,13 @@ class AddressDataMapper implements DataMapperInterface return; } + /** @phpstan-ignore-next-line */ $address->setStreetAddress1($form->getData()); break; case 'streetAddress2': + /** @phpstan-ignore-next-line */ $address->setStreetAddress2($form->getData()); break; diff --git a/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php b/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php index 21d0c3fde..909b671b8 100644 --- a/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php +++ b/src/Bundle/ChillMainBundle/Form/DataMapper/RollingDateDataMapper.php @@ -38,7 +38,7 @@ class RollingDateDataMapper implements DataMapperInterface $forms = iterator_to_array($forms); $viewData = new RollingDate( - $forms['roll']->getData(), + ($forms['roll']->getData() ?? RollingDate::T_TODAY), $forms['fixedDate']->getData() ); } diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php index b422b8649..f5e646dba 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepository.php @@ -11,20 +11,58 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\GeographicalUnit; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\Query\Expr\Join; +use Doctrine\ORM\QueryBuilder; -class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface +final class GeographicalUnitRepository implements GeographicalUnitRepositoryInterface { - private EntityManagerInterface $em; - private EntityRepository $repository; public function __construct(EntityManagerInterface $em) { $this->repository = $em->getRepository($this->getClassName()); - $this->em = $em; + } + + + public function countGeographicalUnitContainingAddress(Address $address): int + { + $qb = $this->buildQueryGeographicalUnitContainingAddress($address); + + return $qb + ->select('COUNT(gu)') + ->getQuery() + ->getSingleScalarResult(); + } + + public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array + { + $qb = $this->buildQueryGeographicalUnitContainingAddress($address); + + return $qb + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->addOrderBy('IDENTITY(gu.layer)') + ->addOrderBy(('gu.unitName')) + ->getQuery() + ->setFirstResult($offset) + ->setMaxResults($limit) + ->getResult(); + } + + private function buildQueryGeographicalUnitContainingAddress(Address $address): QueryBuilder + { + $qb = $this->repository + ->createQueryBuilder('gu') + ; + return $qb + ->select(sprintf('NEW %s(gu.id, gu.unitName, gu.unitRefId, IDENTITY(gu.layer))', GeographicalUnit\SimpleGeographicalUnitDTO::class)) + ->innerJoin(Address::class, 'address', Join::WITH, 'ST_CONTAINS(gu.geom, address.point) = TRUE') + ->where($qb->expr()->eq('address', ':address')) + ->setParameter('address', $address) + ; } public function find($id): ?GeographicalUnit diff --git a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php index cdfb057e2..f2c102407 100644 --- a/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php +++ b/src/Bundle/ChillMainBundle/Repository/GeographicalUnitRepositoryInterface.php @@ -11,8 +11,23 @@ declare(strict_types=1); namespace Chill\MainBundle\Repository; +use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\GeographicalUnit\SimpleGeographicalUnitDTO; use Doctrine\Persistence\ObjectRepository; interface GeographicalUnitRepositoryInterface extends ObjectRepository { + /** + * Return the geographical units as @link{SimpleGeographicalUnitDTO} whithin the address is contained. + * + * This query is executed in real time (without the refresh of the materialized view which load the addresses). + * + * @param Address $address + * @param int $offset + * @param int $limit + * @return SimpleGeographicalUnitDTO[] + */ + public function findGeographicalUnitContainingAddress(Address $address, int $offset = 0, int $limit = 50): array; + + public function countGeographicalUnitContainingAddress(Address $address): int; } diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts new file mode 100644 index 000000000..1499f4a6a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/address.ts @@ -0,0 +1,35 @@ +import {Address, GeographicalUnitLayer, SimpleGeographicalUnit} from "../../types"; +import {fetchResults, makeFetch} from "./apiMethods"; + +export const getAddressById = async (address_id: number): Promise
=> +{ + const url = `/api/1.0/main/address/${address_id}.json`; + + const response = await fetch(url); + + if (response.ok) { + return response.json(); + } + + throw Error('Error with request resource response'); +}; + +export const getGeographicalUnitsByAddress = async (address: Address): Promise => { + return fetchResults(`/api/1.0/main/geographical-unit/by-address/${address.address_id}.json`); +} + +export const getAllGeographicalUnitLayers = async (): Promise => { + return fetchResults(`/api/1.0/main/geographical-unit-layer.json`); +} + +export const syncAddressWithReference = async (address: Address): Promise
=> { + return makeFetch("POST", `/api/1.0/main/address/reference-match/${address.address_id}/sync-with-reference`); +} + +export const markAddressReviewed = async (address: Address): Promise
=> { + return makeFetch("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/reviewed`); +} + +export const markAddressToReview = async (address: Address): Promise
=> { + return makeFetch("POST", `/api/1.0/main/address/reference-match/${address.address_id}/set/to_review`); +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts index 0ef659128..17ac8879e 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/api/apiMethods.ts @@ -67,9 +67,6 @@ export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL }, }; - console.log('for url '+url, body); - console.log('for url '+url, body !== null); - if (body !== null && typeof body !== 'undefined') { Object.assign(opts, {body: JSON.stringify(body)}) } @@ -77,9 +74,6 @@ export const makeFetch = (method: 'POST'|'GET'|'PUT'|'PATCH'|'DEL if (typeof options !== 'undefined') { opts = Object.assign(opts, options); } - console.log('will fetch', url); - console.log('content for ' + url, opts); - return fetch(url, opts) .then(response => { if (response.ok) { diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts new file mode 100644 index 000000000..9bc2409ee --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/address-details/index.ts @@ -0,0 +1,39 @@ +import AddressDetailsButton from "../../vuejs/_components/AddressDetails/AddressDetailsButton.vue"; +import {createApp} from "vue"; +import {createI18n} from "vue-i18n"; +import {_createI18n} from "../../vuejs/_js/i18n"; +import {Address} from "../../types"; + +const i18n = _createI18n({}); + +document.querySelectorAll('span[data-address-details]').forEach((el) => { + const dataset = el.dataset as { + addressId: string, + addressRefStatus: string, + }; + + const app = createApp({ + components: {AddressDetailsButton}, + data() { + return { + addressId: Number.parseInt(dataset.addressId), + addressRefStatus: dataset.addressRefStatus, + } + }, + template: '', + methods: { + onUpdateAddress: (address: Address): void => { + if (address.refStatus === 'to_review' || address.refStatus === 'reviewed') { + // in this two case, the address content do not change + return; + } + if (window.confirm("L'adresse a été modifiée. Vous pouvez continuer votre travail. Cependant, pour afficher les données immédiatement, veuillez recharger la page. \n\n Voulez-vous recharger la page immédiatement ?")) { + window.location.reload(); + } + } + } + }); + + app.use(i18n); + app.mount(el); +}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/types.ts b/src/Bundle/ChillMainBundle/Resources/public/types.ts index 9453c89b0..b31b70897 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/types.ts +++ b/src/Bundle/ChillMainBundle/Resources/public/types.ts @@ -70,6 +70,8 @@ export interface Country { code: string; } +export type AddressRefStatus = 'match'|'to_review'|'reviewed'; + export interface Address { type: "address"; address_id: number; @@ -90,6 +92,13 @@ export interface Address { addressReference: AddressReference | null; validFrom: DateTime; validTo: DateTime | null; + point: Point | null; + refStatus: AddressRefStatus; + isNoAddress: boolean; +} + +export interface AddressWithPoint extends Address { + point: Point } export interface AddressReference { @@ -106,6 +115,19 @@ export interface AddressReference { updatedAt: DateTime | null; } +export interface SimpleGeographicalUnit { + id: number; + layerId: number; + unitName: string; + unitRefId: string; +} + +export interface GeographicalUnitLayer { + id: number; + name: TranslatableString; + refId: string; +} + export interface Location { type: "location"; id: number; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js index b1489bfb6..294ea9480 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/api.js @@ -1,3 +1,5 @@ +import {getAddressById} from 'ChillMainAssets/lib/api/address'; + /** * Endpoint chill_api_single_country__index * method GET, get Country Object @@ -188,13 +190,7 @@ const postPostalCode = (postalCode) => { //<-- * @returns {Promise} a promise containing a Address object */ const getAddress = (id) => { - //console.log('<< get address'); - const url = `/api/1.0/main/address/${id}.json`; - return fetch(url) - .then(response => { - if (response.ok) { return response.json(); } - throw Error('Error with request resource response'); - }); + return getAddressById(id); }; export { diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue index 21ab5e3d0..41279771f 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/Address/components/AddAddress.vue @@ -59,7 +59,7 @@ v-bind:insideModal="false" @pick-address="this.pickAddress" ref="suggestAddress"> - + - + @@ -133,7 +133,7 @@ v-bind:insideModal="false" @getCities="getCities" @getReferenceAddresses="getReferenceAddresses"> - + - + @@ -206,7 +206,7 @@ v-bind:flag="this.flag" v-bind:insideModal="false" ref="dateAddress"> - + - + @@ -580,15 +580,15 @@ export default { this.entity.selected.city = this.context.edit ? this.entity.address.postcode : {}; this.entity.selected.address = {}; - this.entity.selected.address.street = this.context.edit ? this.entity.address.street: null; - this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: null; - this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: null; - this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: null; - this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: null; - this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: null; - this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: null; - 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.address.street = this.context.edit ? this.entity.address.street: ''; + this.entity.selected.address.streetNumber = this.context.edit ? this.entity.address.streetNumber: ''; + this.entity.selected.address.floor = this.context.edit ? this.entity.address.floor: ''; + this.entity.selected.address.corridor = this.context.edit ? this.entity.address.corridor: ''; + this.entity.selected.address.steps = this.context.edit ? this.entity.address.steps: ''; + this.entity.selected.address.flat = this.context.edit ? this.entity.address.flat: ''; + this.entity.selected.address.buildingName = this.context.edit ? this.entity.address.buildingName: ''; + this.entity.selected.address.distribution = this.context.edit ? this.entity.address.distribution: ''; + this.entity.selected.address.extra = this.context.edit ? this.entity.address.extra: ''; 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; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue new file mode 100644 index 000000000..7e060de4c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsButton.vue @@ -0,0 +1,68 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue new file mode 100644 index 000000000..3f6495b0a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressDetailsContent.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue new file mode 100644 index 000000000..6b341d861 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/AddressModal.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsGeographicalLayers.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsGeographicalLayers.vue new file mode 100644 index 000000000..179bed113 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsGeographicalLayers.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue new file mode 100644 index 000000000..fd652a5f9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsMap.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsRefMatching.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsRefMatching.vue new file mode 100644 index 000000000..cbeb9e9e9 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/AddressDetails/Parts/AddressDetailsRefMatching.vue @@ -0,0 +1,96 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue index 2e88bc1cb..14a376856 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/AddressRenderBox.vue @@ -11,17 +11,18 @@

{{ l }}

+

-

{{ address.text }}

-

{{ address.postcode.code }} {{ address.postcode.name }}

-

{{ address.country.name.fr }}

@@ -35,11 +36,12 @@

{{ l }}

+

- {{ address.text }} + {{ address.text }}

@@ -65,11 +67,13 @@ diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml index a8563e5ca..bf372ea06 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_person.yaml @@ -110,6 +110,12 @@ services: tags: - { name: chill.export_filter, alias: person_geog_filter } + Chill\PersonBundle\Export\Filter\PersonFilters\AddressRefStatusFilter: + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: person_address_ref_status_filter } + Chill\PersonBundle\Export\Filter\PersonFilters\ByHouseholdCompositionFilter: tags: - { name: chill.export_filter, alias: person_by_household_composition_filter } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index eb0c044f2..18e3ab13f 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -1062,6 +1062,13 @@ export: Filter persons without household composition: Filtrer les usagers sans composition de ménage (ni ménage) Persons filtered by no composition at %date%: Uniquement les usagers sans composition de ménage à la date du %date% Date calc: Date de calcul + by_address_ref_status: + Filter by person's address ref status: Filtrer par comparaison avec l'adresse de référence + to_review: Diffère de l'adresse de référence + reviewed: Diffère de l'adresse de référence mais conservé par l'utilisateur + match: Identique à l'adresse de référence + Filtered by person\'s address status computed at %datecalc%, only %statuses%: Filtré par comparaison à l'adresse de référence, calculé à %datecalc%, seulement %statuses% + Status: Statut course: by_user_scope: diff --git a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue index 49066a082..0a3831df7 100644 --- a/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue +++ b/src/Bundle/ChillThirdPartyBundle/Resources/public/vuejs/_components/Entity/ThirdPartyRenderBox.vue @@ -1,100 +1,100 @@ @@ -104,75 +104,78 @@ import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue'; import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue'; export default { - name: "ThirdPartyRenderBox", - components: { - AddressRenderBox, - Confidential, - BadgeEntity, - }, - // To avoid components recursively invoking eachother resolve OnTheFly component here - beforeCreate() { - this.$options.components.OnTheFly = require('ChillMainAssets/vuejs/OnTheFly/components/OnTheFly').default; - }, - i18n: { - messages: { - fr: { - children: "Personnes de contact: ", - child_of: "Contact de: ", - }} - }, - props: ['thirdparty', 'options'], - computed: { - isMultiline: function() { - if (this.options.isMultiline){ - return this.options.isMultiline - } else { - return false - } - }, - hasParent() { - return !(this.thirdparty.parent === null || this.thirdparty.parent === undefined); - }, - getProfession() { - let profession = [] - if (this.hasParent && this.thirdparty.profession) { - profession.push(this.thirdparty.profession) - return profession; - } - - if (!this.hasParent && this.thirdparty.category) { - profession = this.thirdparty.category.map((category) => category.text); - } - - return profession; - } - /* TODO need backend normalizer to serve children without circular reference - hasChildren() { - //console.log(this.thirdparty.activeChildren.length > 0) - return false - } */ + name: "ThirdPartyRenderBox", + components: { + AddressRenderBox, + Confidential, + BadgeEntity, + }, + // To avoid components recursively invoking eachother resolve OnTheFly component here + beforeCreate() { + this.$options.components.OnTheFly = require('ChillMainAssets/vuejs/OnTheFly/components/OnTheFly').default; + }, + i18n: { + messages: { + fr: { + children: "Personnes de contact: ", + child_of: "Contact de: ", + } } + }, + props: ['thirdparty', 'options'], + computed: { + isMultiline: function () { + if (this.options.isMultiline) { + return this.options.isMultiline + } else { + return false + } + }, + hasParent() { + return !(this.thirdparty.parent === null || this.thirdparty.parent === undefined); + }, + getProfession() { + let profession = [] + if (this.hasParent && this.thirdparty.profession) { + profession.push(this.thirdparty.profession) + return profession; + } + + if (!this.hasParent && this.thirdparty.category) { + profession = this.thirdparty.category.map((category) => category.text); + } + + return profession; + } + /* TODO need backend normalizer to serve children without circular reference + hasChildren() { + //console.log(this.thirdparty.activeChildren.length > 0) + return false + } */ + } } diff --git a/src/Bundle/import-png.d.ts b/src/Bundle/import-png.d.ts new file mode 100644 index 000000000..6a81ab5d7 --- /dev/null +++ b/src/Bundle/import-png.d.ts @@ -0,0 +1,4 @@ +declare module "*.png" { + const value: any; + export default value; +}