diff --git a/CHANGELOG.md b/CHANGELOG.md index 90f4c2503..42f5a1522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,15 +11,20 @@ and this project adheres to ## Unreleased +* [person] add civility when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) +* [person] add address when creating a person (with the on-the-fly component or in the php form) (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) +* [person] add household creation API point (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/557) + + +## Test releases + +### 2021-04-29 + * [person] prevent circular references in PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/527) * [person] add maritalStatusComment to PersonDocGenNormalizer (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/582) * Load relationships without gender in french fixtures * Add command to remove old draft accompanying periods - - -## Test releases - ### 2021-04-28 * [address] fix bug when editing address: update location and addressreferenceId + better update of the map in edition (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/593) diff --git a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php index 1a7d8956a..1ac7a87fe 100644 --- a/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/AddressReferenceAPIController.php @@ -76,7 +76,8 @@ final class AddressReferenceAPIController extends ApiController protected function customizeQuery(string $action, Request $request, $qb): void { if ($request->query->has('postal_code')) { - $qb->where('e.postcode = :postal_code') + $qb->where($qb->expr()->isNull('e.deletedAt')) + ->andWhere('e.postcode = :postal_code') ->setParameter('postal_code', $request->query->get('postal_code')); } } diff --git a/src/Bundle/ChillMainBundle/Entity/AddressReference.php b/src/Bundle/ChillMainBundle/Entity/AddressReference.php index a9d421fcb..fc4339fe0 100644 --- a/src/Bundle/ChillMainBundle/Entity/AddressReference.php +++ b/src/Bundle/ChillMainBundle/Entity/AddressReference.php @@ -12,13 +12,14 @@ declare(strict_types=1); namespace Chill\MainBundle\Entity; use Chill\MainBundle\Doctrine\Model\Point; +use DateTimeImmutable; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; /** * @ORM\Entity * @ORM\Table(name="chill_main_address_reference", indexes={ - * @ORM\Index(name="address_refid", columns={"refId"}, options={"where": "refid != ''"}) + * @ORM\Index(name="address_refid", columns={"refId"}) * }) * @ORM\HasLifecycleCallbacks */ @@ -33,6 +34,18 @@ class AddressReference */ private string $addressCanonical = ''; + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + * @groups({"read"}) + */ + private ?DateTimeImmutable $createdAt = null; + + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + * @groups({"read"}) + */ + private ?DateTimeImmutable $deletedAt = null; + /** * @ORM\Id * @ORM\GeneratedValue @@ -89,6 +102,22 @@ class AddressReference */ private $streetNumber; + /** + * @ORM\Column(type="datetime_immutable", nullable=true) + * @groups({"read"}) + */ + private ?DateTimeImmutable $updatedAt = null; + + public function getCreatedAt(): ?DateTimeImmutable + { + return $this->createdAt; + } + + public function getDeletedAt(): ?DateTimeImmutable + { + return $this->deletedAt; + } + public function getId(): ?int { return $this->id; @@ -134,6 +163,25 @@ class AddressReference return $this->streetNumber; } + public function getUpdatedAt(): ?DateTimeImmutable + { + return $this->updatedAt; + } + + public function setCreatedAt(?DateTimeImmutable $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function setDeletedAt(?DateTimeImmutable $deletedAt): self + { + $this->deletedAt = $deletedAt; + + return $this; + } + public function setMunicipalityCode(?string $municipalityCode): self { $this->municipalityCode = $municipalityCode; @@ -189,4 +237,11 @@ class AddressReference return $this; } + + public function setUpdatedAt(?DateTimeImmutable $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } } diff --git a/src/Bundle/ChillMainBundle/Entity/PostalCode.php b/src/Bundle/ChillMainBundle/Entity/PostalCode.php index 484a9e322..4b79f58e8 100644 --- a/src/Bundle/ChillMainBundle/Entity/PostalCode.php +++ b/src/Bundle/ChillMainBundle/Entity/PostalCode.php @@ -21,10 +21,11 @@ use Symfony\Component\Serializer\Annotation\Groups; * @ORM\Entity * @ORM\Table( * name="chill_main_postal_code", - * indexes={@ORM\Index( - * name="search_name_code", - * columns={"code", "label"} - * )}) + * indexes={ + * @ORM\Index(name="search_name_code", columns={"code", "label"}), + * @ORM\Index(name="search_by_reference_code", columns={"code", "refpostalcodeid"}) + * }) + * * @ORM\HasLifecycleCallbacks */ class PostalCode diff --git a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php index 9cbad91c9..8a0755902 100644 --- a/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/AddressReferenceRepository.php @@ -130,12 +130,13 @@ final class AddressReferenceRepository implements ObjectRepository $query ->setFromClause('chill_main_address_reference cma') - ->andWhereClause('postcode_id = ?', [$postalCode->getId()]); + ->andWhereClause('postcode_id = ?', [$postalCode->getId()]) + ->andWhereClause('deletedAt IS NULL', []); $pertinenceClause = ['STRICT_WORD_SIMILARITY(addresscanonical, UNACCENT(?))']; $pertinenceArgs = [$pattern]; - $orWhere = ['addresscanonical %>> UNACCENT(?)']; - $orWhereArgs = [$pattern]; + $andWhere = []; + $andWhereArgs = []; foreach (explode(' ', $pattern) as $part) { $part = trim($part); @@ -144,8 +145,8 @@ final class AddressReferenceRepository implements ObjectRepository continue; } - $orWhere[] = "addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%'"; - $orWhereArgs[] = $part; + $andWhere[] = "(addresscanonical LIKE '%' || UNACCENT(LOWER(?)) || '%')"; + $andWhereArgs[] = $part; $pertinenceClause[] = "(EXISTS (SELECT 1 FROM unnest(string_to_array(addresscanonical, ' ')) AS t WHERE starts_with(t, UNACCENT(LOWER(?)))))::int"; $pertinenceClause[] = @@ -154,7 +155,7 @@ final class AddressReferenceRepository implements ObjectRepository } $query ->setSelectPertinence(implode(' + ', $pertinenceClause), $pertinenceArgs) - ->andWhereClause(implode(' OR ', $orWhere), $orWhereArgs); + ->andWhereClause(implode(' AND ', $andWhere), $andWhereArgs); return $query; } diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js index f184bf100..511d2126e 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js @@ -51,9 +51,7 @@ function loadDynamicPicker(element) { }, methods: { addNewEntity(entity) { - console.log('addNewEntity', entity); if (this.multiple) { - console.log('adding multiple'); if (!this.picked.some(el => { return el.type === entity.type && el.id === entity.id; })) { @@ -71,7 +69,6 @@ function loadDynamicPicker(element) { } }, removeEntity(entity) { - console.log('removeEntity', entity); this.picked = this.picked.filter(e => !(e.type === entity.type && e.id === entity.id)); input.value = JSON.stringify(this.picked); }, @@ -86,7 +83,6 @@ function loadDynamicPicker(element) { document.addEventListener('show-hide-show', function(e) { - console.log('creation event caught') loadDynamicPicker(e.detail.container) }) @@ -94,17 +90,14 @@ document.addEventListener('show-hide-hide', function(e) { console.log('hiding event caught') e.detail.container.querySelectorAll('[data-module="pick-dynamic"]').forEach((el) => { let uniqId = el.dataset.uniqid; - console.log(uniqId); if (appsOnPage.has(uniqId)) { appsOnPage.get(uniqId).unmount(); - console.log('App has been unmounted') appsOnPage.delete(uniqId); } }) }) document.addEventListener('DOMContentLoaded', function(e) { - console.log('loaded event', e) loadDynamicPicker(document) }) diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue index 669c225dd..e3991fe2f 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/OnTheFly/components/OnTheFly.vue @@ -210,10 +210,10 @@ export default { let type = this.type, data = {} ; - switch (type) { case 'person': data = this.$refs.castPerson.$data.person; + console.log('person data are', data); break; case 'thirdparty': @@ -238,7 +238,7 @@ export default { if (typeof data.civility !== 'undefined' && null !== data.civility) { data.civility = data.civility !== null ? {type: 'chill_main_civility', id: data.civility.id} : null; } - if (typeof data.civility !== 'undefined' && null !== data.profession) { + if (typeof data.profession !== 'undefined' && null !== data.profession) { data.profession = data.profession !== null ? {type: 'third_party_profession', id: data.profession.id} : null; } // console.log('onthefly data', data); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue index c75001005..e69b26619 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/PickEntity.vue @@ -66,9 +66,18 @@ export default { translatedListOfTypes() { let trans = []; this.types.forEach(t => { - trans.push(appMessages.fr.pick_entity[t].toLowerCase()); + if (this.$props.multiple) { + trans.push(appMessages.fr.pick_entity[t].toLowerCase()); + } else { + trans.push(appMessages.fr.pick_entity[t + '_one'].toLowerCase()); + } }) - return appMessages.fr.pick_entity.modal_title + trans.join(', '); + + if (this.$props.multiple) { + return appMessages.fr.pick_entity.modal_title + trans.join(', '); + } else { + return appMessages.fr.pick_entity.modal_title_one + trans.join(', '); + } } }, methods: { @@ -79,15 +88,10 @@ export default { ); this.$refs.addPersons.resetSearch(); // to cast child method modal.showModal = false; - console.log(this.picked) }, removeEntity(entity) { - console.log('remove entity', entity); this.$emit('removeEntity', entity); } }, - mounted() { - console.log(this.picked); - } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js index f3ae3a928..cc3f324e8 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickEntity/i18n.js @@ -11,6 +11,10 @@ const appMessages = { user: 'Utilisateurs', person: 'Usagers', thirdparty: 'Tiers', + modal_title_one: 'Indiquer un ', + user_one: 'Utilisateur', + thirdparty_one: 'Tiers', + person_one: 'Usager', } } } diff --git a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig index 8bb09623f..1da384920 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig @@ -217,6 +217,7 @@ {% endblock %} {% block pick_entity_dynamic_widget %} + {{ form_help(form)}}
{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig index c11422a1b..f59e0aaff 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/layout.html.twig @@ -36,33 +36,35 @@ {# Flash messages ! #} {% if app.session.flashbag.keys()|length > 0 %} - diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220325134944.php b/src/Bundle/ChillMainBundle/migrations/Version20220325134944.php new file mode 100644 index 000000000..58a20d220 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220325134944.php @@ -0,0 +1,49 @@ +addSql('ALTER TABLE chill_main_address_reference DROP createdAt'); + $this->addSql('ALTER TABLE chill_main_address_reference DROP deletedAt'); + $this->addSql('ALTER TABLE chill_main_address_reference DROP updatedAt'); + $this->addSql('DROP INDEX address_refid'); + $this->addSql('create index address_refid + on chill_main_address_reference (refid) + where ((refid)::text <> \'\'::text)'); + } + + public function getDescription(): string + { + return 'Add 3 fields on AddressReference'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_address_reference ADD createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ADD deletedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_main_address_reference ADD updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL'); + $this->addSql('COMMENT ON COLUMN chill_main_address_reference.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_address_reference.deletedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_address_reference.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('DROP INDEX address_refid'); + $this->addSql('CREATE INDEX address_refid ON chill_main_address_reference (refId)'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220506131307.php b/src/Bundle/ChillMainBundle/migrations/Version20220506131307.php new file mode 100644 index 000000000..f3360d94a --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220506131307.php @@ -0,0 +1,37 @@ +addSql('DROP INDEX chill_internal_address_reference_canonicalized'); + $this->addSql('create index chill_internal_address_reference_canonicalized + on chill_main_address_reference using gist (postcode_id, addresscanonical gist_trgm_ops);'); + } + + public function getDescription(): string + { + return 'Adapt search index on address reference canonicalized'; + } + + public function up(Schema $schema): void + { + $this->addSql('DROP INDEX chill_internal_address_reference_canonicalized'); + $this->addSql('create index chill_internal_address_reference_canonicalized + on chill_main_address_reference using gist (postcode_id, addresscanonical gist_trgm_ops) WHERE deletedat IS NULL;'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220506145935.php b/src/Bundle/ChillMainBundle/migrations/Version20220506145935.php new file mode 100644 index 000000000..9df382214 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220506145935.php @@ -0,0 +1,33 @@ +addSql('DROP INDEX search_by_reference_code'); + } + + public function getDescription(): string + { + return 'Add index to search postal code by references'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE INDEX search_by_reference_code ON chill_main_postal_code (code, refpostalcodeid)'); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index bd53cd8b3..a4a29216c 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -12,12 +12,15 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; +use Chill\PersonBundle\Entity\Household\Household; +use Chill\PersonBundle\Entity\Household\HouseholdMember; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Form\CreationPersonType; use Chill\PersonBundle\Form\PersonType; use Chill\PersonBundle\Privacy\PrivacyEvent; use Chill\PersonBundle\Repository\PersonRepository; use Chill\PersonBundle\Search\SimilarPersonMatcher; +use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter; @@ -31,8 +34,8 @@ use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Security; use Symfony\Component\Validator\Validator\ValidatorInterface; -use Symfony\Contracts\Translation\TranslatorInterface; +use Symfony\Contracts\Translation\TranslatorInterface; use function count; use function hash; use function implode; @@ -248,6 +251,31 @@ final class PersonController extends AbstractController $this->em->flush(); $this->lastPostDataReset(); + $address = $form->get('address')->getData(); + $addressForm = (bool) $form->get('addressForm')->getData(); + + if (null !== $address && $addressForm) { + $household = new Household(); + + $member = new HouseholdMember(); + $member->setPerson($person); + $member->setStartDate(new DateTimeImmutable()); + + $household->addMember($member); + $household->setForceAddress($address); + + $this->em->persist($member); + $this->em->persist($household); + $this->em->flush(); + + if ($form->get('createHousehold')->isClicked()) { + return $this->redirectToRoute('chill_person_household_members_editor', [ + 'persons' => [$person->getId()], + 'household' => $household->getId(), + ]); + } + } + if ($form->get('createPeriod')->isClicked()) { return $this->redirectToRoute('chill_person_accompanying_course_new', [ 'person_id' => [$person->getId()], diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index 4abf5e93c..1fd54f14c 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -12,13 +12,18 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\UserRepository; use Chill\MainBundle\Templating\Entity\UserRender; use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Chill\PersonBundle\Repository\AccompanyingPeriodRepository; +use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\CallbackTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; use Symfony\Component\Form\Extension\Core\Type\FormType; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; @@ -27,11 +32,18 @@ use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Templating\EngineInterface; +use Symfony\Component\Validator\Constraints\NotIdenticalTo; +use Symfony\Component\Validator\Constraints\NotNull; +use function is_int; class ReassignAccompanyingPeriodController extends AbstractController { private AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository; + private AccompanyingPeriodRepository $courseRepository; + + private EntityManagerInterface $em; + private EngineInterface $engine; private FormFactoryInterface $formFactory; @@ -44,8 +56,17 @@ class ReassignAccompanyingPeriodController extends AbstractController private UserRepository $userRepository; - public function __construct(AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, UserRepository $userRepository, EngineInterface $engine, FormFactoryInterface $formFactory, PaginatorFactory $paginatorFactory, Security $security, UserRender $userRender) - { + public function __construct( + AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, + UserRepository $userRepository, + AccompanyingPeriodRepository $courseRepository, + EngineInterface $engine, + FormFactoryInterface $formFactory, + PaginatorFactory $paginatorFactory, + Security $security, + UserRender $userRender, + EntityManagerInterface $em + ) { $this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository; $this->engine = $engine; $this->formFactory = $formFactory; @@ -53,6 +74,8 @@ class ReassignAccompanyingPeriodController extends AbstractController $this->security = $security; $this->userRepository = $userRepository; $this->userRender = $userRender; + $this->courseRepository = $courseRepository; + $this->em = $em; } /** @@ -68,23 +91,55 @@ class ReassignAccompanyingPeriodController extends AbstractController $form->handleRequest($request); - $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod( - $form['user']->getData() - ); + $userFrom = $form['user']->getData(); + + $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); $paginator = $this->paginatorFactory->create($total); $periods = $this->accompanyingPeriodACLAwareRepository ->findByUserOpenedAccompanyingPeriod( - $form['user']->getData(), + $userFrom, ['openingDate' => 'ASC'], $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() ); + $periodIds = []; + + foreach ($periods as $period) { + $periodIds[] = $period->getId(); + } + + // Create an array of period id's to pass into assignForm hiddenfield + $assignForm = $this->buildReassignForm($periodIds, $userFrom); + + $assignForm->handleRequest($request); + + if ($assignForm->isSubmitted() && $assignForm->isValid()) { + $assignPeriodIds = json_decode($assignForm->get('periods')->getData(), true); + $userTo = $assignForm->get('userTo')->getData(); + $userFrom = $assignForm->get('userFrom')->getData(); + + foreach ($assignPeriodIds as $periodId) { + $period = $this->courseRepository->find($periodId); + + if ($period->getUser() === $userFrom) { + $period->setUser($userTo); + } + } + + $this->em->flush(); + + // redirect to the first page + return $this->redirectToRoute('chill_course_list_reassign', $request->query->all()); + } + return new Response( $this->engine->render('@ChillPerson/AccompanyingPeriod/reassign_list.html.twig', [ + 'assignForm' => $assignForm->createView(), + 'form' => $form->createView(), 'paginator' => $paginator, 'periods' => $periods, - 'form' => $form->createView(), + 'userFrom' => $userFrom, ]) ); } @@ -98,17 +153,63 @@ class ReassignAccompanyingPeriodController extends AbstractController 'method' => 'get', 'csrf_protection' => false, ]); $builder - ->add('user', EntityType::class, [ - 'class' => User::class, - 'choices' => $this->userRepository->findByActive(['username' => 'ASC']), - 'choice_label' => function (User $u) { - return $this->userRender->renderString($u, []); - }, + ->add('user', PickUserDynamicType::class, [ 'multiple' => false, - 'label' => 'User', + 'label' => 'reassign.Current user', 'required' => false, + 'help' => 'reassign.Choose a user and click on "Filter" to apply', ]); return $builder->getForm(); } + + private function buildReassignForm(array $periodIds, ?User $userFrom): FormInterface + { + $defaultData = [ + 'userFrom' => $userFrom, + 'periods' => json_encode($periodIds), + ]; + + $builder = $this->formFactory->createNamedBuilder('reassign', FormType::class, $defaultData); + + if (null !== $userFrom) { + $constraints = [new NotIdenticalTo(['value' => $userFrom])]; + } else { + $constraints = []; + } + + $builder + ->add('periods', HiddenType::class) + ->add('userFrom', HiddenType::class) + ->add('userTo', PickUserDynamicType::class, [ + 'multiple' => false, + 'label' => 'reassign.Next user', + 'required' => true, + 'help' => 'reassign.All periods on this list will be reassigned to this user, excepted the one you manually reassigned before', + 'constraints' => [new NotNull()], + ]); + + $builder->get('userFrom')->addModelTransformer(new CallbackTransformer( + static function (?User $user) { + if (null === $user) { + return ''; + } + + return $user->getId(); + }, + function (?string $id) { + if (null === $id) { + return null; + } + + if (!is_int((int) $id)) { + throw new TransformationFailedException('the user id is not a numeric'); + } + + return $this->userRepository->find((int) $id); + } + )); + + return $builder->getForm(); + } } diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php index 4fa9802f8..18b2788f6 100644 --- a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadRelationships.php @@ -21,6 +21,7 @@ use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\ORM\EntityManagerInterface; use Doctrine\Persistence\ObjectManager; +use function array_key_exists; use function count; class LoadRelationships extends Fixture implements DependentFixtureInterface @@ -34,7 +35,7 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface $this->em = $em; } - public function getDependencies() + public function getDependencies(): array { return [ LoadPeople::class, @@ -42,9 +43,11 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface ]; } - public function load(ObjectManager $manager) + public function load(ObjectManager $manager): void { - for ($i = 0; 15 > $i; ++$i) { + $existing = []; + + for ($i = 0; 20 > $i; ++$i) { $user = $this->getRandomUser(); $date = new DateTimeImmutable(); $relationship = (new Relationship()) @@ -57,6 +60,17 @@ class LoadRelationships extends Fixture implements DependentFixtureInterface ->setUpdatedBy($user) ->setCreatedAt($date) ->setUpdatedAt($date); + + // remove the potential duplicates + $set = $relationship->getFromPerson()->getId() < $relationship->getToPerson()->getId() ? + [$relationship->getFromPerson()->getId(), $relationship->getToPerson()->getId()] : + [$relationship->getToPerson()->getId(), $relationship->getFromPerson()->getId()]; + + if (array_key_exists($set[0], $existing) && array_key_exists($set[1], $existing[$set[0]])) { + continue; + } + + $existing[$set[0]][$existing[$set[1]]] = 1; $manager->persist($relationship); } diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 6d2bf6d18..99c408ff1 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -555,6 +555,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac 'methods' => [ Request::METHOD_GET => true, Request::METHOD_HEAD => true, + Request::METHOD_POST => true, ], ], 'suggestHouseholdByAccompanyingPeriodParticipation' => [ @@ -873,6 +874,12 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac AccompanyingPeriodVoter::EDIT, AccompanyingPeriodVoter::DELETE, ], + AccompanyingPeriodVoter::REASSIGN_BULK => [ + AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, + ], + AccompanyingPeriodVoter::TOGGLE_CONFIDENTIAL => [ + AccompanyingPeriodVoter::CONFIDENTIAL_CRUD, + ], ], ]); } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php index 22c33f85f..2de526ee6 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdMember.php @@ -35,7 +35,7 @@ class HouseholdMember /** * @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) * @Serializer\Groups({"read", "docgen:read"}) - * @Assert\GreaterThan( + * @Assert\GreaterThanOrEqual( * propertyPath="startDate", * message="household_membership.The end date must be after start date", * groups={"household_memberships"} @@ -82,14 +82,13 @@ class HouseholdMember /** * @ORM\ManyToOne(targetEntity=Position::class) * @Serializer\Groups({"read", "docgen:read"}) - * @Assert\NotNull(groups={"household_memberships_created"}) */ private ?Position $position = null; /** * @ORM\Column(type="boolean", name="sharedhousehold") */ - private bool $shareHousehold = false; + private bool $shareHousehold = true; /** * @ORM\Column(type="date_immutable", nullable=true, options={"default": null}) @@ -201,15 +200,18 @@ class HouseholdMember return $this; } - public function setPosition(Position $position): self + public function setPosition(?Position $position): self { - if ($this->position instanceof Position) { + if ($this->position instanceof Position && $this->position !== $position) { throw new LogicException('The position is already set. You cannot change ' . 'a position of a membership'); } $this->position = $position; - $this->shareHousehold = $position->getShareHousehold(); + + if (null !== $position) { + $this->shareHousehold = $position->getShareHousehold(); + } return $this; } diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 2aed9df97..124db26eb 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -11,10 +11,13 @@ declare(strict_types=1); namespace Chill\PersonBundle\Form; +use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Form\Event\CustomizeFormEvent; use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillPhoneNumberType; +use Chill\MainBundle\Form\Type\PickAddressType; use Chill\MainBundle\Form\Type\PickCenterType; +use Chill\MainBundle\Form\Type\PickCivilityType; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Form\Type\GenderType; @@ -24,9 +27,12 @@ use libphonenumber\PhoneNumberType; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints\Callback; +use Symfony\Component\Validator\Context\ExecutionContextInterface; final class CreationPersonType extends AbstractType { @@ -55,6 +61,11 @@ final class CreationPersonType extends AbstractType $builder ->add('firstName') ->add('lastName') + ->add('civility', PickCivilityType::class, [ + 'required' => false, + 'label' => 'Civility', + 'placeholder' => 'choose civility', + ]) ->add('gender', GenderType::class, [ 'required' => true, 'placeholder' => null, ]) @@ -71,6 +82,17 @@ final class CreationPersonType extends AbstractType ]) ->add('email', EmailType::class, [ 'required' => false, + ]) + ->add('addressForm', CheckboxType::class, [ + 'label' => 'Create a household and add an address', + 'required' => false, + 'mapped' => false, + 'help' => 'A new household will be created. The person will be member of this household.', + ]) + ->add('address', PickAddressType::class, [ + 'required' => false, + 'mapped' => false, + 'label' => false, ]); if ($this->askCenters) { @@ -97,6 +119,9 @@ final class CreationPersonType extends AbstractType { $resolver->setDefaults([ 'data_class' => Person::class, + 'constraints' => [ + new Callback([$this, 'validateCheckedAddress']), + ], ]); } @@ -107,4 +132,18 @@ final class CreationPersonType extends AbstractType { return self::NAME; } + + public function validateCheckedAddress($data, ExecutionContextInterface $context, $payload): void + { + /** @var bool $addressFrom */ + $addressFrom = $context->getObject()->get('addressForm')->getData(); + /** @var ?Address $address */ + $address = $context->getObject()->get('address')->getData(); + + if ($addressFrom && null === $address) { + $context->buildViolation('person_creation.If you want to create an household, an address is required') + ->atPath('addressForm') + ->addViolation(); + } + } } diff --git a/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php b/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php index 99c698c91..192142043 100644 --- a/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php +++ b/src/Bundle/ChillPersonBundle/Form/HouseholdMemberType.php @@ -26,12 +26,15 @@ class HouseholdMemberType extends AbstractType 'input' => 'datetime_immutable', ]); - if (!$options['data']->getPosition()->getShareHousehold()) { - $builder->add('endDate', ChillDateType::class, [ - 'label' => 'household.End date', - 'input' => 'datetime_immutable', - ]); + if (null !== $options['data']->getPosition()) { + if (!$options['data']->getPosition()->getShareHousehold()) { + $builder->add('endDate', ChillDateType::class, [ + 'label' => 'household.End date', + 'input' => 'datetime_immutable', + ]); + } } + $builder ->add('comment', ChillTextareaType::class, [ 'label' => 'household.Comment', diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php index 1c7cbb538..8ae41a98b 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php @@ -55,7 +55,7 @@ class MembersEditor $this->eventDispatcher = $eventDispatcher; } - public function addMovement(DateTimeImmutable $date, Person $person, Position $position, ?bool $holder = false, ?string $comment = null): self + public function addMovement(DateTimeImmutable $date, Person $person, ?Position $position, ?bool $holder = false, ?string $comment = null): self { if (null === $this->household) { throw new LogicException('You must define a household first'); @@ -69,68 +69,70 @@ class MembersEditor ->setComment($comment); $this->household->addMember($membership); - if ($position->getShareHousehold()) { - // launch event only if moving to a "share household" position, - // and if the destination household is different than the previous one - $event = new PersonAddressMoveEvent($person); - $event->setNextMembership($membership); + if (null !== $position) { + if ($position->getShareHousehold()) { + // launch event only if moving to a "share household" position, + // and if the destination household is different than the previous one + $event = new PersonAddressMoveEvent($person); + $event->setNextMembership($membership); - $counter = 0; + $counter = 0; - foreach ($person->getHouseholdParticipationsShareHousehold() as $participation) { - if ($participation === $membership) { - continue; - } + foreach ($person->getHouseholdParticipationsShareHousehold() as $participation) { + if ($participation === $membership) { + continue; + } - if ($participation->getStartDate() > $membership->getStartDate()) { - continue; - } + if ($participation->getStartDate() > $membership->getStartDate()) { + continue; + } - ++$counter; + ++$counter; - if ($participation->getEndDate() === null || $participation->getEndDate() > $date) { - $participation->setEndDate($date); - $this->membershipsAffected[] = $participation; - $this->oldMembershipsHashes[] = spl_object_hash($participation); + if ($participation->getEndDate() === null || $participation->getEndDate() > $date) { + $participation->setEndDate($date); + $this->membershipsAffected[] = $participation; + $this->oldMembershipsHashes[] = spl_object_hash($participation); - if ($participation->getHousehold() !== $this->household) { - $event->setPreviousMembership($participation); - $this->events[] = $event; + if ($participation->getHousehold() !== $this->household) { + $event->setPreviousMembership($participation); + $this->events[] = $event; + } } } - } - // send also the event if there was no participation before - if (0 === $counter) { - $this->events[] = $event; - } - - foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) { - if ($participation->getHousehold() === $this->household - && $participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate() - && $participation->getStartDate() <= $membership->getStartDate() - ) { - $participation->setEndDate($membership->getStartDate()); - } - } - } else { - // if a members is moved to the same household than the one he belongs to, - // we should make it leave the household - if ($person->getCurrentHousehold($date) === $this->household) { - $this->leaveMovement($date, $person); - } - - // if there are multiple belongings not sharing household, close the others - foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) { - if ($participation === $membership) { - continue; + // send also the event if there was no participation before + if (0 === $counter) { + $this->events[] = $event; } - if ($participation->getHousehold() === $this->household - && ($participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate()) - && $participation->getStartDate() <= $membership->getStartDate() - ) { - $participation->setEndDate($membership->getStartDate()); + foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) { + if ($participation->getHousehold() === $this->household + && $participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate() + && $participation->getStartDate() <= $membership->getStartDate() + ) { + $participation->setEndDate($membership->getStartDate()); + } + } + } else { + // if a members is moved to the same household than the one he belongs to, + // we should make it leave the household + if ($person->getCurrentHousehold($date) === $this->household) { + $this->leaveMovement($date, $person); + } + + // if there are multiple belongings not sharing household, close the others + foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) { + if ($participation === $membership) { + continue; + } + + if ($participation->getHousehold() === $this->household + && ($participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate()) + && $participation->getStartDate() <= $membership->getStartDate() + ) { + $participation->setEndDate($membership->getStartDate()); + } } } } diff --git a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php index c95c5caf5..722598ef7 100644 --- a/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/SectionMenuBuilder.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Menu; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Knp\Menu\MenuItem; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; @@ -64,13 +65,15 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface ]); } - $menu->addChild($this->translator->trans('Accompanying courses of users'), [ - 'route' => 'chill_course_list_reassign', - ]) - ->setExtras([ - 'order' => 12, - 'icons' => ['task'], - ]); + if ($this->authorizationChecker->isGranted(AccompanyingPeriodVoter::REASSIGN_BULK, null)) { + $menu->addChild($this->translator->trans('reassign.Bulk reassign'), [ + 'route' => 'chill_course_list_reassign', + ]) + ->setExtras([ + 'order' => 40, + 'icons' => [], + ]); + } } public static function getMenuIds(): array diff --git a/src/Bundle/ChillPersonBundle/Resources/public/page/person/create-person.js b/src/Bundle/ChillPersonBundle/Resources/public/page/person/create-person.js new file mode 100644 index 000000000..7e29c4530 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/page/person/create-person.js @@ -0,0 +1,18 @@ +import { ShowHide } from 'ShowHide'; + +const addressForm = document.getElementById("addressForm"); +const address = document.getElementById("address"); + +new ShowHide({ + froms: [addressForm], + container: [address], + test: function(froms) { + for (let f of froms.values()) { + for (let input of f.querySelectorAll('input').values()) { + return input.checked; + } + } + return false; + }, + event_name: 'change' +}); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue index 1a1a7e26e..ff8aef961 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/PersonsAssociated/ParticipationItem.vue @@ -123,6 +123,7 @@ export default { body.email = payload.data.email; body.altNames = payload.data.altNames; body.gender = payload.data.gender; + body.civility = payload.data.civility; makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body) .then(response => { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue index df2bd0c35..605ab6443 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Resources/ResourceItem.vue @@ -150,6 +150,7 @@ export default { body.email = payload.data.email; body.altNames = payload.data.altNames; body.gender = payload.data.gender; + body.civility = payload.data.civility; makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body) .then(response => { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.js index 20ed1c8da..338094122 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_api/OnTheFly.js @@ -14,8 +14,13 @@ const getPersonAltNames = () => fetch('/api/1.0/person/config/alt_names.json').then(response => { if (response.ok) { return response.json(); } throw Error('Error with request resource response'); - });; + }); +const getCivilities = () => + fetch('/api/1.0/main/civility.json').then(response => { + if (response.ok) { return response.json(); } + throw Error('Error with request resource response'); + }); /* * POST a new person @@ -56,6 +61,7 @@ const patchPerson = (id, body) => { export { getPerson, getPersonAltNames, + getCivilities, postPerson, patchPerson }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue index 90505a1e2..f84fc2e1d 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/AddPersons.vue @@ -277,12 +277,79 @@ export default { } }, saveFormOnTheFly({ type, data }) { - // console.log('saveFormOnTheFly from addPersons, type', type, ', data', data); + console.log('saveFormOnTheFly from addPersons, type', type, ', data', data); if (type === 'person') { + makeFetch('POST', '/api/1.0/person/person.json', data) - .then(response => { - this.newPriorSuggestion(response); + .then(responsePerson => { + + this.newPriorSuggestion(responsePerson); this.$refs.onTheFly.closeModal(); + + if (null !== data.addressId) { + const household = { + 'type': 'household' + }; + const address = { + 'id': data.addressId + }; + makeFetch('POST', '/api/1.0/person/household.json', household) + .then(responseHousehold => { + const member = { + 'concerned': [ + { + 'person': { + 'type': 'person', + 'id': responsePerson.id + }, + 'start_date': { + // TODO: use date.js methods (low priority) + 'datetime': `${new Date().toISOString().split('T')[0]}T00:00:00+02:00` + }, + 'holder': false, + 'comment': null + } + ], + 'destination': { + 'type': 'household', + 'id': responseHousehold.id + }, + 'composition': null + }; + return makeFetch('POST', '/api/1.0/person/household/members/move.json', member) + .then(_response => { + makeFetch('POST', `/api/1.0/person/household/${responseHousehold.id}/address.json`, address) + .then(_response => {}) + .catch((error) => { + if (error.name === 'ValidationException') { + for (let v of error.violations) { + this.$toast.open({message: v }); + } + } else { + this.$toast.open({message: 'An error occurred'}); + } + }); + }) + .catch((error) => { + if (error.name === 'ValidationException') { + for (let v of error.violations) { + this.$toast.open({message: v }); + } + } else { + this.$toast.open({message: 'An error occurred'}); + } + }); + }) + .catch((error) => { + if (error.name === 'ValidationException') { + for (let v of error.violations) { + this.$toast.open({message: v }); + } + } else { + this.$toast.open({message: 'An error occurred'}); + } + }); + } }) .catch((error) => { if (error.name === 'ValidationException') { @@ -292,7 +359,8 @@ export default { } else { this.$toast.open({message: 'An error occurred'}); } - }) + }); + } else if (type === 'thirdparty') { makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue index e6fa85902..5a8708cc3 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/OnTheFly/Person.vue @@ -87,6 +87,20 @@ +{{ $t('person.address.warning') }}
+