Merge remote-tracking branch 'origin/master' into features/household-validation

This commit is contained in:
2021-06-18 12:02:34 +02:00
83 changed files with 3539 additions and 647 deletions

View File

@@ -138,11 +138,15 @@ class LoadActivity extends AbstractFixture implements OrderedFixtureInterface, C
foreach($persons as $person) {
$activityNbr = rand(0,3);
$ref = 'activity_'.$person->getFullnameCanonical();
for($i = 0; $i < $activityNbr; $i ++) {
print "Creating an activity type for : ".$person."\n";
print "Creating an activity type for : ".$person." (ref: ".$ref.") \n";
$activity = $this->newRandomActivity($person);
$manager->persist($activity);
}
$this->setReference($ref, $activity);
}
$manager->flush();
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Chill\ActivityBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Chill\ActivityBundle\Entity\Activity;
use Chill\MainBundle\DataFixtures\ORM\LoadAbstractNotificationsTrait;
use Chill\ActivityBundle\DataFixtures\ORM\LoadActivity;
/**
* Load notififications into database
*/
class LoadActivityNotifications extends AbstractFixture implements DependentFixtureInterface
{
use LoadAbstractNotificationsTrait;
public $notifs = [
[
'message' => 'Hello !',
'entityClass' => Activity::class,
'entityRef' => 'activity_gerard depardieu',
'sender' => 'center a_social',
'addressees' => [
'center a_administrative',
'center a_direction',
'multi_center'
],
]
];
public function getDependencies()
{
return [
LoadActivity::class,
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Chill\MainBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Intl\Intl;
use Chill\MainBundle\Entity\Notification;
/**
* Load notififications into database
*/
trait LoadAbstractNotificationsTrait
{
public function load(ObjectManager $manager)
{
foreach ($this->notifs as $notif) {
$entityId = $this->getReference($notif['entityRef'])->getId();
print('Adding notification for '.$notif['entityClass'].'(entity id:'.$entityId.")\n");
$newNotif = (new Notification())
->setMessage($notif['message'])
->setSender($this->getReference($notif['sender']))
->setRelatedEntityClass($notif['entityClass'])
->setRelatedEntityId($entityId)
->setDate(new \DateTimeImmutable('now'))
->setRead([])
;
foreach ($notif['addressees'] as $addressee) {
$newNotif->addAddressee($this->getReference($addressee));
}
$manager->persist($newNotif);
$manager->flush();
}
}
}

View File

@@ -115,7 +115,6 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
$loader->load('services.yaml');
$loader->load('services/doctrine.yaml');
$loader->load('services/logger.yaml');
$loader->load('services/repositories.yaml');
$loader->load('services/pagination.yaml');
$loader->load('services/export.yaml');
$loader->load('services/form.yaml');
@@ -278,7 +277,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_POST => true,
Request::METHOD_HEAD => true
Request::METHOD_HEAD => true,
Request::METHOD_PATCH => true
]
],
]
@@ -320,7 +320,8 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface,
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true
Request::METHOD_HEAD => true,
Request::METHOD_POST => true,
]
],
]

View File

@@ -11,7 +11,7 @@ use Chill\ThirdPartyBundle\Entity\ThirdParty;
/**
* Address
*
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="chill_main_address")
* @ORM\HasLifecycleCallbacks()
*/

View File

@@ -7,7 +7,7 @@ use Chill\MainBundle\Doctrine\Model\Point;
use Symfony\Component\Serializer\Annotation\Groups;
/**
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="chill_main_address_reference")
* @ORM\HasLifecycleCallbacks()
*/

View File

@@ -25,7 +25,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @ORM\Entity(repositoryClass="Chill\MainBundle\Repository\CenterRepository")
* @ORM\Entity
* @ORM\Table(name="centers")
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>

View File

@@ -8,7 +8,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
/**
* Country
*
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="country")
* @ORM\Cache(usage="READ_ONLY", region="country_cache_region")
* @ORM\HasLifecycleCallbacks()

View File

@@ -27,7 +27,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="group_centers")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
*

View File

@@ -25,7 +25,7 @@ use Doctrine\ORM\Mapping as ORM;
/**
* Language
*
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="language")
* @ORM\Cache(usage="READ_ONLY", region="language_cache_region")
* @ORM\HasLifecycleCallbacks()

View File

@@ -0,0 +1,187 @@
<?php
/*
* Copyright (C) 2021 Champs-Libres <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Entity\User;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(
* name="chill_main_notification",
* uniqueConstraints={
* @ORM\UniqueConstraint(columns={"relatedEntityClass", "relatedEntityId"})
* }
* )
*/
class Notification
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private int $id;
/**
* @ORM\Column(type="text")
*/
private string $message;
/**
* @ORM\Column(type="datetime_immutable")
*/
private \DateTimeImmutable $date;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
*/
private User $sender;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
*/
private Collection $addressees;
/**
* @ORM\Column(type="string", length=255)
*/
private string $relatedEntityClass;
/**
* @ORM\Column(type="integer")
*/
private int $relatedEntityId;
/**
* @ORM\Column(type="json")
*/
private array $read;
public function __construct()
{
$this->addressees = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getMessage(): ?string
{
return $this->message;
}
public function setMessage(string $message): self
{
$this->message = $message;
return $this;
}
public function getDate(): ?\DateTimeImmutable
{
return $this->date;
}
public function setDate(\DateTimeImmutable $date): self
{
$this->date = $date;
return $this;
}
public function getSender(): ?User
{
return $this->sender;
}
public function setSender(?User $sender): self
{
$this->sender = $sender;
return $this;
}
/**
* @return Collection|User[]
*/
public function getAddressees(): Collection
{
return $this->addressees;
}
public function addAddressee(User $addressee): self
{
if (!$this->addressees->contains($addressee)) {
$this->addressees[] = $addressee;
}
return $this;
}
public function removeAddressee(User $addressee): self
{
$this->addressees->removeElement($addressee);
return $this;
}
public function getRelatedEntityClass(): ?string
{
return $this->relatedEntityClass;
}
public function setRelatedEntityClass(string $relatedEntityClass): self
{
$this->relatedEntityClass = $relatedEntityClass;
return $this;
}
public function getRelatedEntityId(): ?int
{
return $this->relatedEntityId;
}
public function setRelatedEntityId(int $relatedEntityId): self
{
$this->relatedEntityId = $relatedEntityId;
return $this;
}
public function getRead(): array
{
return $this->read;
}
public function setRead(array $read): self
{
$this->read = $read;
return $this;
}
}

View File

@@ -27,7 +27,7 @@ use Chill\MainBundle\Entity\RoleScope;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="permission_groups")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
*

View File

@@ -8,7 +8,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
/**
* PostalCode
*
* @ORM\Entity(repositoryClass="Chill\MainBundle\Repository\PostalCodeRepository")
* @ORM\Entity
* @ORM\Table(
* name="chill_main_postal_code",
* indexes={@ORM\Index(
@@ -26,7 +26,7 @@ class PostalCode
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $id;
@@ -34,7 +34,7 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=255, name="label")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $name;
@@ -42,7 +42,7 @@ class PostalCode
* @var string
*
* @ORM\Column(type="string", length=100)
* @groups({"read"})
* @groups({"write", "read"})
*/
private $code;
@@ -50,10 +50,17 @@ class PostalCode
* @var Country
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country")
* @groups({"read"})
* @groups({"write", "read"})
*/
private $country;
/**
* @var integer
*
* @ORM\Column(name="origin", type="integer", nullable=true)
* @groups({"write", "read"})
*/
private $origin = 0;
/**
* Get id
@@ -65,6 +72,32 @@ class PostalCode
return $this->id;
}
/**
* Set origin
*
* @param int $origin
*
* @return PostalCode
*/
public function setOrigin($origin)
{
$this->origin = $origin;
return $this;
}
/**
* Get origin
*
* @return int
*/
public function getOrigin()
{
return $this->origin;
}
/**
* Set name
*

View File

@@ -25,7 +25,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="role_scopes")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
*

View File

@@ -28,7 +28,7 @@ use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* @ORM\Entity()
* @ORM\Entity
* @ORM\Table(name="scopes")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
* @DiscriminatorMap(typeProperty="type", mapping={

View File

@@ -12,7 +12,7 @@ use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
/**
* User
*
* @ORM\Entity(repositoryClass="Chill\MainBundle\Repository\UserRepository")
* @ORM\Entity
* @ORM\Table(name="users")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="acl_cache_region")
* @DiscriminatorMap(typeProperty="type", mapping={

View File

@@ -19,26 +19,18 @@
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\Center;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\TransformationFailedException;
/**
* Transform a center object to his id, and vice-versa
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CenterTransformer implements DataTransformerInterface
{
/**
*
* @var ObjectManager
*/
private $om;
public function __construct(ObjectManager $om)
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $em)
{
$this->om = $om;
$this->em = $em;
}
public function reverseTransform($id)
@@ -46,15 +38,17 @@ class CenterTransformer implements DataTransformerInterface
if ($id === NULL) {
return NULL;
}
$center = $this->om->getRepository('ChillMainBundle:Center')
->find($id);
$center = $this
->em
->getRepository(Center::class)
->find($id);
if ($center === NULL) {
throw new TransformationFailedException(sprintf(
'No center found with id %d', $id));
}
return $center;
}
@@ -63,7 +57,7 @@ class CenterTransformer implements DataTransformerInterface
if ($center === NULL) {
return '';
}
return $center->getId();
}

View File

@@ -24,28 +24,20 @@ use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Persistence\ObjectManager;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\EntityManagerInterface;
class MultipleObjectsToIdTransformer implements DataTransformerInterface
{
/**
* @var ObjectManager
*/
private $em;
/**
* @var string
*/
private $class;
/**
* @param ObjectManager $em
*/
public function __construct(ObjectManager $em, $class)
private EntityManagerInterface $em;
private ?string $class;
public function __construct(EntityManagerInterface $em, ?string $class = null)
{
$this->em = $em;
$this->class = $class;
}
/**
* Transforms an object (use) to a string (id).
*

View File

@@ -20,28 +20,20 @@
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Doctrine\Persistence\ObjectManager;
class ObjectToIdTransformer implements DataTransformerInterface
{
/**
* @var ObjectManager
*/
private $om;
private EntityManagerInterface $em;
/**
* @var string
*/
private $class;
private ?string $class;
/**
* @param ObjectManager $om
*/
public function __construct(ObjectManager $om, $class)
public function __construct(EntityManagerInterface $em, ?string $class = null)
{
$this->om = $om;
$this->em = $em;
$this->class = $class;
}
@@ -73,7 +65,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
return null;
}
$object = $this->om
$object = $this->em
->getRepository($this->class)
->find($id)
;

View File

@@ -19,41 +19,28 @@
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\Scope;
use Symfony\Component\Form\DataTransformerInterface;
use Doctrine\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityManagerInterface;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class ScopeTransformer implements DataTransformerInterface
{
/**
*
* @var ObjectManager
*/
protected $om;
/**
*
* @var TranslatableStringHelper
*/
protected $helper;
public function __construct(ObjectManager $om)
private EntityManagerInterface $em;
public function __construct(EntityManagerInterface $em)
{
$this->om = $om;
$this->em = $em;
}
public function transform($scope)
{
if ($scope === NULL) {
return NULL;
}
return $scope->getId();
}
@@ -62,15 +49,17 @@ class ScopeTransformer implements DataTransformerInterface
if ($id == NULL) {
return NULL;
}
$scope = $this->om->getRepository('ChillMainBundle:Scope')
->find($id);
$scope = $this
->em
->getRepository(Scope::class)
->find($id);
if ($scope === NULL) {
throw new TransformationFailedException(sprintf("The scope with id "
. "'%d' were not found", $id));
}
return $scope;
}

View File

@@ -22,6 +22,7 @@ use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\DataMapper\ScopePickerDataMapper;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Doctrine\ORM\EntityRepository;
@@ -60,7 +61,7 @@ class ScopePickerType extends AbstractType
protected $tokenStorage;
/**
* @var EntityRepository
* @var ScopeRepository
*/
protected $scopeRepository;
@@ -72,7 +73,7 @@ class ScopePickerType extends AbstractType
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
EntityRepository $scopeRepository,
ScopeRepository $scopeRepository,
TranslatableStringHelper $translatableStringHelper
) {
$this->authorizationHelper = $authorizationHelper;

View File

@@ -25,6 +25,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
use Symfony\Component\Security\Core\Role\Role;
@@ -53,16 +54,12 @@ class UserPickerType extends AbstractType
*/
protected $tokenStorage;
/**
*
* @var \Chill\MainBundle\Repository\UserRepository
*/
protected $userRepository;
protected UserRepository $userRepository;
public function __construct(
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
EntityRepository $userRepository
UserRepository $userRepository
) {
$this->authorizationHelper = $authorizationHelper;
$this->tokenStorage = $tokenStorage;

View File

@@ -1,50 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\AddressReference;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
/**
* @method AddressReference|null find($id, $lockMode = null, $lockVersion = null)
* @method AddressReference|null findOneBy(array $criteria, array $orderBy = null)
* @method AddressReference[] findAll()
* @method AddressReference[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class AddressReferenceRepository extends ServiceEntityRepository
final class AddressReferenceRepository implements ObjectRepository
{
public function __construct(ManagerRegistry $registry)
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
parent::__construct($registry, AddressReference::class);
$this->repository = $entityManager->getRepository(AddressReference::class);
}
// /**
// * @return AddressReference[] Returns an array of AddressReference objects
// */
/*
public function findByExampleField($value)
public function find($id, $lockMode = null, $lockVersion = null): ?AddressReference
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->orderBy('a.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
return $this->repository->find($id, $lockMode, $lockVersion);
}
*/
/*
public function findOneBySomeField($value): ?AddressReference
public function findOneBy(array $criteria, array $orderBy = null): ?AddressReference
{
return $this->createQueryBuilder('a')
->andWhere('a.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return AddressReference[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return AddressReference[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return AddressReference::class;
}
*/
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Address;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class AddressRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Address::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Address
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Address
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Address[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Address[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Address::class;
}
}

View File

@@ -1,27 +1,50 @@
<?php
/*
* Copyright (C) 2018 Champs-Libres <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
/**
*
*
*/
class CenterRepository extends \Doctrine\ORM\EntityRepository
use Chill\MainBundle\Entity\Center;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class CenterRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Center::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Center
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Center
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Center[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Center[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Center::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Country;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class CountryRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Country::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Country
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Country
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Country[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Country[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Country::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class GroupCenterRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(GroupCenter::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?GroupCenter
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?GroupCenter
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return GroupCenter[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return GroupCenter[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return GroupCenter::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Language;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class LanguageRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Language::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Language
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Language
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Language[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Language[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Language::class;
}
}

View File

@@ -0,0 +1,66 @@
<?php
/*
* Copyright (C) 2021 Champs-Libres <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Notification;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class NotificationRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Notification::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Notification
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Notification
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Notification[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Notification[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Notification::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\PermissionsGroup;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class PermissionsGroupRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PermissionsGroup::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?PermissionsGroup
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?PermissionsGroup
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return PermissionsGroup[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return PermissionsGroup[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return PermissionsGroup::class;
}
}

View File

@@ -1,28 +1,50 @@
<?php
/*
* Copyright (C) 2018 Champs-Libres <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class PostalCodeRepository extends \Doctrine\ORM\EntityRepository
use Chill\MainBundle\Entity\PostalCode;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class PostalCodeRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PostalCode::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?PostalCode
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?PostalCode
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return PostalCode[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return PostalCode[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return PostalCode::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\RoleScope;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class RoleScopeRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(RoleScope::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?RoleScope
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?RoleScope
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return RoleScope[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return RoleScope[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return RoleScope::class;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\Scope;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class ScopeRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(Scope::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?Scope
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?Scope
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return Scope[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return Scope[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return Scope::class;
}
}

View File

@@ -1,121 +1,148 @@
<?php
/*
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\GroupCenter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
/**
*
*
*/
class UserRepository extends \Doctrine\ORM\EntityRepository
final class UserRepository implements ObjectRepository
{
public function countByUsernameOrEmail($pattern)
private EntityRepository $repository;
private EntityManagerInterface $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
$this->repository = $entityManager->getRepository(User::class);
}
public function find($id, $lockMode = null, $lockVersion = null): ?User
{
return $this->repository->find($id, $lockMode, $lockVersion);
}
public function findOneBy(array $criteria, array $orderBy = null): ?User
{
return $this->repository->findOneBy($criteria, $orderBy);
}
/**
* @return User[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @return User[]
*/
public function findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function getClassName() {
return User::class;
}
public function countByUsernameOrEmail(string $pattern): int
{
$qb = $this->queryByUsernameOrEmail($pattern);
$qb->select('COUNT(u)');
return (int) $qb->getQuery()->getSingleScalarResult();
}
public function findByUsernameOrEmail($pattern)
public function findByUsernameOrEmail(string $pattern)
{
$qb = $this->queryByUsernameOrEmail($pattern);
return $qb->getQuery()->getResult();
}
public function findOneByUsernameOrEmail($pattern)
public function findOneByUsernameOrEmail(string $pattern)
{
$qb = $this->queryByUsernameOrEmail($pattern);
return $qb->getQuery()->getSingleResult();
}
/**
* Get the users having a specific flags
*
* If provided, only the users amongst "filtered users" are searched. This
* allows to make a first search amongst users based on role and center
*
* If provided, only the users amongst "filtered users" are searched. This
* allows to make a first search amongst users based on role and center
* and, then filter those users having some flags.
*
*
* @param \Chill\MainBundle\Entity\User[] $amongstUsers
*/
public function findUsersHavingFlags($flag, $amongstUsers = [])
public function findUsersHavingFlags($flag, array $amongstUsers = []): array
{
$gcs = $this->_em->createQuery("SELECT DISTINCT gc "
. "FROM ".GroupCenter::class." gc "
. "JOIN gc.permissionsGroup pg "
. "WHERE "
. "JSONB_EXISTS_IN_ARRAY(pg.flags, :flag) = :true ")
$gcs = $this
->entityManager
->createQuery(
"SELECT DISTINCT gc " .
"FROM ".GroupCenter::class." gc " .
"JOIN gc.permissionsGroup pg " .
"WHERE " .
"JSONB_EXISTS_IN_ARRAY(pg.flags, :flag) = :true "
)
->setParameters([
'true' => true,
'flag' => $flag
])
->getResult();
if (count($gcs) === 0) {
return [];
}
$qb = $this->_em->createQueryBuilder();
$qb = $this->entityManager->createQueryBuilder();
$qb
->select('DISTINCT u')
->from(User::class, 'u')
->where("u.enabled = 'TRUE'")
;
->where("u.enabled = 'TRUE'");
$orx = $qb->expr()->orX();
foreach($gcs as $i => $gc) {
$orx->add(':gc_'.$i.' MEMBER OF u.groupCenters');
$qb->setParameter('gc_'.$i, $gc);
$orx->add(':gc_' . $i . ' MEMBER OF u.groupCenters');
$qb->setParameter('gc_' . $i, $gc);
}
$qb->andWhere($orx);
if (count($amongstUsers) > 0) {
if ($amongstUsers !== []) {
$qb
->andWhere($qb->expr()->in('u', ':amongstUsers'))
->setParameter('amongstUsers', $amongstUsers)
;
->setParameter('amongstUsers', $amongstUsers);
}
return $qb->getQuery()->getResult();
}
protected function queryByUsernameOrEmail($pattern)
protected function queryByUsernameOrEmail(string $pattern): QueryBuilder
{
$qb = $this->createQueryBuilder('u');
$qb = $this->entityManager->createQueryBuilder('u');
$searchByPattern = $qb->expr()->orX();
$searchByPattern
->add($qb->expr()->eq('u.usernameCanonical', 'LOWER(UNACCENT(:pattern))'))
->add($qb->expr()->eq('u.emailCanonical', 'LOWER(UNACCENT(:pattern))'))
;
->add($qb->expr()->eq('u.emailCanonical', 'LOWER(UNACCENT(:pattern))'));
$qb
->where($searchByPattern)
->setParameter('pattern', $pattern)
;
->setParameter('pattern', $pattern);
return $qb;
}
}

View File

@@ -252,6 +252,9 @@ div.address_form {
display: flex;
flex-direction: column;
flex-grow: 1;
div.custom-address, div.custom-postcode {
padding: 12px;
}
}
div.address_form__select__map {
@@ -259,13 +262,20 @@ div.address_form {
div#address_map {
height:400px;
width:400px;
input {
border: 1px solid #999;
}
}
}
}
div.address_form__more {
& > div {
display: flex;
& > label {
width: 30%;
}
}
}
}

View File

@@ -1,33 +1,73 @@
<template>
<div v-if="address.address">
{{ address.address.street }}, {{ address.address.streetNumber }}
<div class='person__address__create'>
<div>
<h2 v-if="!edit">{{ $t('create_a_new_address') }}</h2>
<h2 v-else>{{ $t('edit_a_new_address') }}</h2>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</div>
<div>
<show-address
v-if="address"
v-bind:address="address">
</show-address>
</div>
</div>
<div v-if="address.city">
{{ address.city.code }} {{ address.city.name }}
</div>
<div v-if="address.country">
{{ address.country.name }}
<div v-if="!edit" class='person__address__valid'>
<h2>{{ $t('date') }}</h2>
<input
type="date"
name="validFrom"
:placeholder="$t('validFrom')"
v-model="validFrom"/>
<div v-if="errors.length > 0">
{{ errors }}
</div>
</div>
<div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a :href=backUrl class="sc-button bt-cancel">{{ $t('back_to_the_list') }}</a>
</li>
<li v-if="!edit">
<button type="submit" class="sc-button bt-update centered" @click="addToPerson">
{{ $t('add_an_address_to_person') }}
</button>
</li>
</ul>
</div>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</template>
<script>
import { mapState } from 'vuex';
import AddAddress from '../_components/AddAddress.vue';
import ShowAddress from '../_components/ShowAddress.vue';
export default {
name: 'App',
components: {
AddAddress
AddAddress,
ShowAddress
},
data() {
return {
edit: window.mode === 'edit',
personId: window.personId,
addressId: window.addressId,
backUrl: `/fr/person/${window.personId}/address/list`, //TODO better way to pass this
validFrom: new Date().toISOString().split('T')[0]
}
},
computed: {
address() {
return this.$store.state.address;
},
errors() {
return this.$store.state.errorMsg;
}
},
methods: {
@@ -36,9 +76,9 @@ export default {
let newAddress = {
'isNoAddress': address.isNoAddress,
'street': address.selected.address.street,
'streetNumber': address.selected.address.streetNumber,
'postcode': {'id': address.selected.city.id },
'street': address.isNoAddress ? '' : address.street,
'streetNumber': address.isNoAddress ? '' : address.streetNumber,
'postcode': {'id': address.selected.city.id},
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
@@ -49,12 +89,47 @@ export default {
};
if (address.selected.address.point !== undefined){
newAddress = Object.assign(newAddress, {'point': address.selected.address.point.coordinates});
newAddress = Object.assign(newAddress, {
'point': address.selected.address.point.coordinates
});
}
if (address.writeNewPostalCode){
let newPostalCode = address.newPostalCode;
newPostalCode = Object.assign(newPostalCode, {
'country': {'id': address.selected.country.id },
});
newAddress = Object.assign(newAddress, {
'newPostalCode': newPostalCode
});
}
if (this.edit){
this.$store.dispatch('updateAddress', {
addressId: this.addressId,
newAddress: newAddress
});
} else {
this.$store.dispatch('addAddress', newAddress);
}
this.$store.dispatch('addAddress', newAddress);
modal.showModal = false;
},
addToPerson() {
this.$store.dispatch('addDateToAddressAndAddressToPerson', {
personId: this.personId,
addressId: this.$store.state.address.address_id,
body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}}
})
},
getEditAddress() {
this.$store.dispatch('getEditAddress', this.addressId);
}
}
},
mounted() {
if (this.edit) {
this.getEditAddress();
}
},
};
</script>

View File

@@ -1,20 +1,32 @@
const addressMessages = {
fr: {
add_an_address_title: 'Ajouter une adresse',
add_an_address_title: 'Créer une adresse',
edit_an_address_title: 'Modifier une adresse',
create_a_new_address: 'Créer une nouvelle adresse',
edit_a_new_address: 'Modifier l\'adresse',
select_an_address_title: 'Sélectionner une adresse',
fill_an_address: 'Compléter l\'adresse',
select_country: 'Choisir le pays',
select_city: 'Choisir une localité',
select_address: 'Choisir une adresse',
create_address: 'Appuyer sur "Entrée" pour créer une nouvelle adresse',
create_address: 'Adresse inconnue. Cliquez ici pour créer une nouvelle adresse',
isNoAddress: 'Pas d\'adresse complète',
street: 'Nom de rue',
streetNumber: 'Numéro',
floor: 'Étage',
corridor: 'Couloir',
steps: 'Escalier',
flat: 'Appartement',
buildingName: 'Nom du batiment',
extra: 'Complément d\'adresse',
distribution: 'Service particulier de distribution'
distribution: 'Service particulier de distribution',
create_postal_code: 'Localité inconnue. Cliquez ici pour créer une nouvelle localité',
postalCode_name: 'Nom de la localité',
postalCode_code: 'Code postal de la localité',
date: 'Date de la nouvelle adresse',
add_an_address_to_person: 'Ajouter l\'adresse à la personne',
validFrom: 'Date de la nouvelle adresse',
back_to_the_list: 'Retour à la liste'
}
};

View File

@@ -1,7 +1,7 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postAddress } from '../../_api/AddAddress'
import { patchAddress, postAddress, postPostalCode, postAddressToPerson, getAddress } from '../../_api/AddAddress'
const debug = process.env.NODE_ENV !== 'production';
@@ -9,6 +9,8 @@ const store = createStore({
strict: debug,
state: {
address: {},
editAddress: {}, //TODO or should be address?
person: {},
errorMsg: []
},
getters: {
@@ -20,25 +22,119 @@ const store = createStore({
addAddress(state, address) {
console.log('@M addAddress address', address);
state.address = address;
}
},
updateAddress(state, address) {
console.log('@M updateAddress address', address);
state.address = address;
},
addAddressToPerson(state, person) {
console.log('@M addAddressToPerson person', person);
state.person = person;
},
addDateToAddress(state, validFrom) {
console.log('@M addDateToAddress address.validFrom', validFrom);
state.validFrom = validFrom;
},
getEditAddress(state, address) {
console.log('@M getEditAddress address', address);
state.editAddress = address;
},
},
actions: {
addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload);
//commit('addAddress', payload); // à remplacer par la suite
//fetch POST qui envoie l'adresse, et récupère la confirmation que c'est ok.
//La confirmation est l'adresse elle-même.
if('newPostalCode' in payload){
let postalCodeBody = payload.newPostalCode;
postalCodeBody = Object.assign(postalCodeBody, {'origin': 3});
postPostalCode(postalCodeBody)
.then(postalCode => {
let body = payload;
body.postcode = {'id': postalCode.id},
postAddress(body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
})
postAddress(payload)
} else {
postAddress(payload)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
},
addDateToAddressAndAddressToPerson({ commit }, payload) {
console.log('@A addDateToAddressAndAddressToPerson payload', payload);
patchAddress(payload.addressId, payload.body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
commit('addDateToAddress', address.validFrom);
resolve();
}))
}).then(
postAddressToPerson(payload.personId, payload.addressId)
.then(person => new Promise((resolve, reject) => {
commit('addAddressToPerson', person);
resolve();
}))
.catch((error) => {
commit('catchError', error);
})
))
.catch((error) => {
commit('catchError', error);
});
}
},
updateAddress({ commit }, payload) {
console.log('@A updateAddress payload', payload);
if('newPostalCode' in payload.newAddress){ // TODO change the condition because it writes new postal code in edit mode now: !writeNewPostalCode
let postalCodeBody = payload.newAddress.newPostalCode;
postalCodeBody = Object.assign(postalCodeBody, {'origin': 3});
postPostalCode(postalCodeBody)
.then(postalCode => {
let body = payload.newAddress;
body.postcode = {'id': postalCode.id },
patchAddress(payload.addressId, body)
.then(address => new Promise((resolve, reject) => {
commit('updateAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
})
} else {
patchAddress(payload.addressId, payload.newAddress)
.then(address => new Promise((resolve, reject) => {
commit('updateAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
},
getEditAddress({ commit }, payload) {
console.log('@A getEditAddress payload', payload);
getAddress(payload).then(address => new Promise((resolve, reject) => {
commit('getEditAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
},
}
});

View File

@@ -36,7 +36,6 @@ const fetchCities = (country) => {
*/
const fetchReferenceAddresses = (postalCode) => {
console.log('<<< fetching references addresses for', postalCode);
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address-reference.json?item_per_page=1000&postal_code=${postalCode.id}`;
return fetch(url)
.then(response => {
@@ -45,16 +44,75 @@ const fetchReferenceAddresses = (postalCode) => {
});
};
/*
* Endpoint chill_api_single_address_reference__index
* method GET, get AddressReference Object
* @returns {Promise} a promise containing all AddressReference objects filtered with postal code
*/
const fetchAddresses = () => {
console.log('<<< fetching addresses');
//TODO deal with huge number of addresses... we should do suggestion...
const url = `/api/1.0/main/address.json?item_per_page=1000`;
return fetch(url)
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__entity__create
* method POST, post Address Object
* @returns {Promise}
*/
const postAddress = (address) => {
console.log(address);
const url = `/api/1.0/main/address.json?`;
const body = address;
return fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
}).then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__entity__create
* method PATCH, patch Address Instance
*
* @id integer - id of address
* @body Object - dictionary with changes to post
*/
const patchAddress = (id, body) => {
const url = `/api/1.0/main/address/${id}.json`;
return fetch(url, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_postal_code__entity_create
* method POST, post Postal Code Object
* @returns {Promise}
*/
const postPostalCode = (postalCode) => {
const url = `/api/1.0/main/postal-code.json?`;
const body = postalCode;
return fetch(url, {
method: 'POST',
headers: {
@@ -67,9 +125,55 @@ const postAddress = (address) => {
});
};
/*
* Endpoint chill_api_single_person_address
* method POST, post Person instance
*
* @id integer - id of Person
* @body Object - dictionary with changes to post
*/
const postAddressToPerson = (personId, addressId) => {
console.log(personId);
console.log(addressId);
const body = {
'id': addressId
};
const url = `/api/1.0/person/person/${personId}/address.json`
return fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json;charset=utf-8'},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};
/*
* Endpoint chill_api_single_address__index
* method GET, get Address Object
* @params {id} the address id
* @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');
});
};
export {
fetchCountries,
fetchCities,
fetchReferenceAddresses,
postAddress
fetchAddresses,
postAddress,
patchAddress,
postPostalCode,
postAddressToPerson,
getAddress
};

View File

@@ -1,7 +1,10 @@
<template>
<button class="sc-button bt-create centered mt-4" @click="openModal">
<button v-if="!edit" class="sc-button bt-create mt-4" @click="openModal">
{{ $t('add_an_address_title') }}
</button>
<button v-else class="sc-button bt-create mt-4" @click="openModal">
{{ $t('edit_an_address_title') }}
</button>
<teleport to="body">
<modal v-if="modal.showModal"
@@ -9,7 +12,8 @@
@close="modal.showModal = false">
<template v-slot:header>
<h3 class="modal-title">{{ $t('add_an_address_title') }}</h3>
<h3 v-if="!edit" class="modal-title">{{ $t('add_an_address_title') }}</h3>
<h3 v-if="edit" class="modal-title">{{ $t('edit_an_address_title') }}</h3>
</template>
<template v-slot:body>
@@ -42,6 +46,7 @@
</city-selection>
<address-selection
v-if="!isNoAddress"
v-bind:address="address"
v-bind:updateMapCenter="updateMapCenter">
</address-selection>
@@ -100,11 +105,14 @@ export default {
emits: ['addNewAddress'],
data() {
return {
edit: window.mode === 'edit',
modal: {
showModal: false,
modalDialogClass: "modal-dialog-scrollable modal-xl"
},
address: {
writeNewAddress: false,
writeNewPostalCode: false,
loaded: {
countries: [],
cities: [],
@@ -115,11 +123,17 @@ export default {
city: {},
address: {},
},
newPostalCode: {
code: null,
name: null
},
addressMap: {
center : [48.8589, 2.3469], // Note: LeafletJs demands [lat, lon] cfr https://macwright.com/lonlat/
zoom: 12
},
isNoAddress: false,
street: null,
streetNumber: null,
floor: null,
corridor: null,
steps: null,
@@ -167,8 +181,8 @@ export default {
getCities(country) {
console.log('getCities for', country.name);
fetchCities(country).then(cities => new Promise((resolve, reject) => {
this.address.loaded.cities = cities.results;
resolve()
this.address.loaded.cities = cities.results.filter(c => c.origin !== 3); // filter out user-defined cities
resolve();
}))
.catch((error) => {
this.errorMsg.push(error.message);
@@ -198,6 +212,23 @@ export default {
this.address.loaded.cities = [];
this.address.selected.city = {};
this.address.selected.country = {};
this.address.isNoAddress = this.edit ? this.$store.state.editAddress.isNoAddress: false;;
this.address.street = this.edit ? this.$store.state.editAddress.street: null;
this.address.streetNumber = this.edit ? this.$store.state.editAddress.streetNumber: null;
this.address.floor = this.edit ? this.$store.state.editAddress.floor: null;
this.address.corridor = this.edit ? this.$store.state.editAddress.corridor: null;
this.address.steps = this.edit ? this.$store.state.editAddress.steps: null;
this.address.flat = this.edit ? this.$store.state.editAddress.flat: null;
this.address.buildingName = this.edit ? this.$store.state.editAddress.buildingName: null;
this.address.distribution = this.edit ? this.$store.state.editAddress.distribution: null;
this.address.extra = this.edit ? this.$store.state.editAddress.extra: null;
this.address.writeNewAddress = this.edit;
this.address.writeNewPostalCode = this.edit;
this.address.newPostalCode = this.edit ?
{
code: this.$store.state.editAddress.postcode !== undefined ? this.$store.state.editAddress.postcode.code : null,
name: this.$store.state.editAddress.postcode !== undefined ? this.$store.state.editAddress.postcode.name : null
} : {};
console.log('cities and addresses', this.address.loaded.cities, this.address.loaded.addresses);
}
}

View File

@@ -40,7 +40,7 @@ export default {
update() {
console.log('update map with : ', this.address.addressMap.center)
marker.setLatLng(this.address.addressMap.center);
map.setView(this.address.addressMap.center, 12);
map.setView(this.address.addressMap.center, 15);
}
},
mounted(){

View File

@@ -1,37 +1,56 @@
<template>
<h4>{{ $t('fill_an_address') }}</h4>
<div>
<h4>{{ $t('fill_an_address') }}</h4>
<input
<label for="floor">{{ $t('floor') }}</label>
<input
type="text"
name="floor"
:placeholder="$t('floor')"
v-model="floor"/>
<input
</div>
<div>
<label for="corridor">{{ $t('corridor') }}</label>
<input
type="text"
name="corridor"
:placeholder="$t('corridor')"
v-model="corridor"/>
<input
</div>
<div>
<label for="steps">{{ $t('steps') }}</label>
<input
type="text"
name="steps"
:placeholder="$t('steps')"
v-model="steps"/>
<input
</div>
<div>
<label for="flat">{{ $t('flat') }}</label>
<input
type="text"
name="flat"
:placeholder="$t('flat')"
v-model="flat"/>
<input
</div>
<div>
<label for="buildingName">{{ $t('buildingName') }}</label>
<input
type="text"
name="buildingName"
:placeholder="$t('buildingName')"
v-model="buildingName"/>
<input
</div>
<div>
<label for="extra">{{ $t('extra') }}</label>
<input
type="text"
name="extra"
:placeholder="$t('extra')"
v-model="extra"/>
<input
</div>
<div>
<label for="distribution">{{ $t('distribution') }}</label>
<input
type="text"
name="distribution"
:placeholder="$t('distribution')"

View File

@@ -15,6 +15,18 @@
:options="addresses">
</VueMultiselect>
</div>
<div class="custom-address" v-if="writeNewAddress || writeNewPostalCode">
<input
type="text"
name="street"
:placeholder="$t('street')"
v-model="street"/>
<input
type="text"
name="streetNumber"
:placeholder="$t('streetNumber')"
v-model="streetNumber"/>
</div>
</template>
<script>
@@ -30,9 +42,31 @@ export default {
}
},
computed: {
writeNewAddress() {
return this.address.writeNewAddress;
},
writeNewPostalCode() {
return this.address.writeNewPostalCode;
},
addresses() {
return this.address.loaded.addresses;
}
},
street: {
set(value) {
this.address.street = value;
},
get() {
return this.address.street;
}
},
streetNumber: {
set(value) {
this.address.streetNumber = value;
},
get() {
return this.address.streetNumber;
}
},
},
methods: {
transName(value) {
@@ -40,14 +74,12 @@ export default {
},
selectAddress(value) {
this.address.selected.address = value;
this.address.street = value.street;
this.address.streetNumber = value.streetNumber;
this.updateMapCenter(value.point);
},
addAddress (newAddress) {
const address = {
street: newAddress
};
this.value = address;
this.address.selected.address = address;
addAddress() {
this.address.writeNewAddress = true;
}
}
};

View File

@@ -8,9 +8,25 @@
label="value"
:custom-label="transName"
:placeholder="$t('select_city')"
:taggable="true"
:multiple="false"
@tag="addPostalCode"
:tagPlaceholder="$t('create_postal_code')"
:options="cities">
</VueMultiselect>
</div>
<div class="custom-postcode" v-if="writeNewPostalCode">
<input
type="text"
name="name"
:placeholder="$t('postalCode_name')"
v-model="name"/>
<input
type="text"
name="code"
:placeholder="$t('postalCode_code')"
v-model="code"/>
</div>
</template>
<script>
@@ -25,18 +41,42 @@ export default {
value: null
}
},
computed: {
writeNewPostalCode() {
return this.address.writeNewPostalCode;
},
cities() {
return this.address.loaded.cities;
},
name: {
set(value) {
this.address.newPostalCode.name = value;
},
get() {
return this.address.newPostalCode.name;
}
},
code: {
set(value) {
this.address.newPostalCode.code= value;
},
get() {
return this.address.newPostalCode.code;
}
},
},
methods: {
transName(value) {
return `${value.code}-${value.name}`
},
selectCity(value) {
this.address.selected.city = value;
this.address.newPostalCode.name = value.name;
this.address.newPostalCode.code = value.code;
this.getReferenceAddresses(value);
},
},
computed: {
cities() {
return this.address.loaded.cities;
addPostalCode() {
this.address.writeNewPostalCode = true;
}
}
};

View File

@@ -22,13 +22,18 @@ export default {
props: ['address', 'getCities'],
data() {
return {
value: this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
edit: window.mode === 'edit',
defaultCountry: this.edit ? this.$store.state.editAddress.country.code : 'FR',
value: this.address.loaded.countries.filter(c => c.countryCode === this.defaultCountry)[0]
}
},
methods: {
init() {
this.value = this.edit ?
this.address.loaded.countries.filter(c => c.countryCode === this.$store.state.editAddress.country.code)[0]:
this.address.loaded.countries.filter(c => c.countryCode === 'FR')[0]
if (this.value !== undefined) {
this.getCities(this.value);
this.selectCountry(this.value);
}
},
transName ({ name }) {

View File

@@ -0,0 +1,44 @@
<template>
<div v-if="address.text">
{{ address.text }}
</div>
<div v-if="address.postcode">
{{ address.postcode.name }}
</div>
<div v-if="address.country">
{{ address.country.name.fr }}
</div>
<div v-if="address.floor">
<span>{{ $t('floor') }}</span>: {{ address.floor }}
</div>
<div v-if="address.corridor">
<span>{{ $t('corridor') }}</span>: {{ address.corridor }}
</div>
<div v-if="address.steps">
<span>{{ $t('steps') }}</span>: {{ address.steps }}
</div>
<div v-if="address.flat">
<span>{{ $t('flat') }}</span>: {{ address.flat }}
</div>
<div v-if="address.buildingName">
<span>{{ $t('buildingName') }}</span>: {{ address.buildingName }}
</div>
<div v-if="address.extra">
<span>{{ $t('extra') }}</span>: {{ address.extra }}
</div>
<div v-if="address.distribution">
<span>{{ $t('distribution') }}</span>: {{ address.distribution }}
</div>
</template>
<script>
export default {
name: 'ShowAddress',
props: ['address'],
data() {
return {
}
},
};
</script>

View File

@@ -14,8 +14,20 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
public function normalize($address, string $format = null, array $context = [])
{
$data['address_id'] = $address->getId();
$data['text'] = $address->getStreet().', '.$address->getBuildingName();
$data['text'] = $address->getStreet().', '.$address->getStreetNumber();
$data['street'] = $address->getStreet();
$data['streetNumber'] = $address->getStreetNumber();
$data['postcode']['name'] = $address->getPostCode()->getName();
$data['postcode']['code'] = $address->getPostCode()->getCode();
$data['country']['name'] = $address->getPostCode()->getCountry()->getName();
$data['country']['code'] = $address->getPostCode()->getCountry()->getCountryCode();
$data['floor'] = $address->getFloor();
$data['corridor'] = $address->getCorridor();
$data['steps'] = $address->getSteps();
$data['flat'] = $address->getBuildingName();
$data['buildingName'] = $address->getFlat();
$data['distribution'] = $address->getDistribution();
$data['extra'] = $address->getExtra();
return $data;
}
@@ -25,5 +37,5 @@ class AddressNormalizer implements NormalizerAwareInterface, NormalizerInterface
return $data instanceof Address;
}
}

View File

@@ -17,6 +17,93 @@ components:
type: integer
name:
type: string
Address:
type: object
properties:
address_id:
type: integer
text:
type: string
postcode:
type: object
properties:
name:
type: string
Country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
PostalCode:
type: object
properties:
id:
type: integer
name:
type: string
code:
type: string
country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
AddressReference:
type: object
properties:
id:
type: integer
refId:
type: string
street:
type: string
streetNumber:
type: string
postcode:
type: object
properties:
id:
type: integer
name:
type: string
code:
type: string
country:
type: object
properties:
id:
type: integer
name:
type: object
countryCode:
type: string
municipalityCode:
type: string
source:
type: string
point:
type: object
properties:
type:
type: string
coordinates:
type: array
items:
type: number
minItems: 2
maxItems: 2
paths:
/1.0/search.json:
@@ -29,7 +116,7 @@ paths:
description: >
**Warning**: This is currently a stub (not really implemented
The search is performed across multiple entities. The entities must be listed into
The search is performed across multiple entities. The entities must be listed into
`type` parameters.
The results are ordered by relevance, from the most to the lowest relevant.
@@ -55,4 +142,286 @@ paths:
responses:
200:
description: "OK"
/1.0/main/address.json:
get:
tags:
- address
summary: Return a list of all Chill addresses
responses:
200:
description: "ok"
post:
tags:
- address
summary: create a new address
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
buildingName:
type: string
corridor:
type: string
distribution:
type: string
extra:
type: string
flat:
type: string
floor:
type: string
isNoAddress:
type: boolean
point:
type: array
items:
type: number
minItems: 2
maxItems: 2
postcode:
$ref: '#/components/schemas/PostalCode'
steps:
type: string
street:
type: string
streetNumber:
type: string
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
patch:
tags:
- address
summary: patch an address
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
buildingName:
type: string
corridor:
type: string
distribution:
type: string
extra:
type: string
flat:
type: string
floor:
type: string
isNoAddress:
type: boolean
point:
type: array
items:
type: number
minItems: 2
maxItems: 2
postcode:
$ref: '#/components/schemas/PostalCode'
steps:
type: string
street:
type: string
streetNumber:
type: string
validFrom:
type: string
validTo:
type: string
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/main/address/{id}.json:
get:
tags:
- address
summary: Return an address by id
parameters:
- name: id
in: path
required: true
description: The address id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Address'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/address-reference.json:
get:
tags:
- address
summary: Return a list of all reference addresses
parameters:
- in: query
name: postal_code
required: false
schema:
type: integer
description: The id of a postal code to filter the reference addresses
responses:
200:
description: "ok"
/1.0/main/address-reference/{id}.json:
get:
tags:
- address
summary: Return a reference address by id
parameters:
- name: id
in: path
required: true
description: The reference address id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/AddressReference'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/postal-code.json:
get:
tags:
- address
summary: Return a list of all postal-code
parameters:
- in: query
name: country
required: false
schema:
type: integer
description: The id of a country to filter the postal code
responses:
200:
description: "ok"
post:
tags:
- address
summary: create a new PostalCode
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
code:
type: string
country:
$ref: '#/components/schemas/Country'
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/main/postal-code/{id}.json:
get:
tags:
- address
summary: Return a postal code by id
parameters:
- name: id
in: path
required: true
description: The postal code id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/PostalCode'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/main/country.json:
get:
tags:
- address
summary: Return a list of all countries
responses:
200:
description: "ok"
/1.0/main/country/{id}.json:
get:
tags:
- address
summary: Return a country by id
parameters:
- name: id
in: path
required: true
description: The country id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Country'
404:
description: "not found"
401:
description: "Unauthorized"

View File

@@ -2,17 +2,33 @@ parameters:
# cl_chill_main.example.class: Chill\MainBundle\Example
services:
_defaults:
autowire: true
autoconfigure: true
Chill\MainBundle\Repository\:
resource: '../Repository/'
autowire: true
autoconfigure: true
Chill\MainBundle\Serializer\Normalizer\:
resource: '../Serializer/Normalizer'
autoconfigure: true
autowire: true
tags:
- { name: 'serializer.normalizer', priority: 64 }
Chill\MainBundle\Form\Type\:
resource: '../Form/Type'
autoconfigure: true
autowire: true
tags:
- { name: form.type }
Chill\MainBundle\Doctrine\Event\:
resource: '../Doctrine/Event/'
autowire: true
tags:
tags:
- { name: 'doctrine.event_subscriber' }
chill.main.helper.translatable_string:

View File

@@ -65,8 +65,6 @@ services:
chill.main.form.choice_loader.postal_code:
class: Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader
arguments:
- '@Chill\MainBundle\Repository\PostalCodeRepository'
chill.main.form.type.export:
class: Chill\MainBundle\Form\Type\Export\ExportType
@@ -98,32 +96,10 @@ services:
arguments:
- '@Chill\MainBundle\Export\ExportManager'
chill.main.form.date_type:
class: Chill\MainBundle\Form\Type\ChillDateType
tags:
- { name: form.type }
chill.main.form.pick_user_type:
class: Chill\MainBundle\Form\Type\UserPickerType
arguments:
- "@chill.main.security.authorization.helper"
- "@security.token_storage"
- "@chill.main.user_repository"
tags:
- { name: form.type }
chill.main.form.pick_scope_type:
class: Chill\MainBundle\Form\Type\ScopePickerType
arguments:
- "@chill.main.security.authorization.helper"
- "@security.token_storage"
- "@chill.main.scope_repository"
- "@chill.main.helper.translatable_string"
tags:
- { name: form.type }
chill.main.form.advanced_search_type:
class: Chill\MainBundle\Form\AdvancedSearchType
autowire: true
autoconfigure: true
arguments:
- "@chill_main.search_provider"
tags:

View File

@@ -1,35 +0,0 @@
services:
chill.main.countries_repository:
class: Doctrine\ORM\EntityRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- "Chill\\MainBundle\\Entity\\Country"
chill.main.user_repository:
class: Doctrine\ORM\EntityRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- "Chill\\MainBundle\\Entity\\User"
chill.main.scope_repository:
class: Doctrine\ORM\EntityRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- "Chill\\MainBundle\\Entity\\Scope"
chill.main.postalcode_repository:
class: Doctrine\ORM\EntityRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- "Chill\\MainBundle\\Entity\\PostalCode"
Chill\MainBundle\Repository\PostalCodeRepository: '@chill.main.postalcode_repository'
chill.main.center_repository:
class: Doctrine\ORM\EntityRepository
factory: ["@doctrine.orm.entity_manager", getRepository]
arguments:
- "Chill\\MainBundle\\Entity\\Center"
Chill\MainBundle\Repository\CenterRepository: '@chill.main.center_repository'

View File

@@ -32,9 +32,8 @@ services:
- { name: twig.extension }
Chill\MainBundle\Templating\Entity\CommentRender:
arguments:
- '@chill.main.user_repository'
- '@Symfony\Component\Templating\EngineInterface'
autoconfigure: true
autowire: true
tags:
- { name: 'chill.render_entity' }

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Add table for ChillMain/Notification
*/
final class Version20210610140248 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add table for ChillMain/Notification';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_notification_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_notification (id INT NOT NULL, sender_id INT NOT NULL, message TEXT NOT NULL, date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, relatedEntityClass VARCHAR(255) NOT NULL, relatedEntityId INT NOT NULL, read JSONB NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_5BDC8067F624B39D ON chill_main_notification (sender_id)');
$this->addSql('CREATE UNIQUE INDEX UNIQ_5BDC8067567988B4440F6072 ON chill_main_notification (relatedEntityClass, relatedEntityId)');
$this->addSql('COMMENT ON COLUMN chill_main_notification.date IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('CREATE TABLE chill_main_notification_addresses_user (notification_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(notification_id, user_id))');
$this->addSql('CREATE INDEX IDX_E52C5D2BEF1A9D84 ON chill_main_notification_addresses_user (notification_id)');
$this->addSql('CREATE INDEX IDX_E52C5D2BA76ED395 ON chill_main_notification_addresses_user (user_id)');
$this->addSql('ALTER TABLE chill_main_notification ADD CONSTRAINT FK_5BDC8067F624B39D FOREIGN KEY (sender_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_notification_addresses_user ADD CONSTRAINT FK_E52C5D2BEF1A9D84 FOREIGN KEY (notification_id) REFERENCES chill_main_notification (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_notification_addresses_user ADD CONSTRAINT FK_E52C5D2BA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_notification_addresses_user DROP CONSTRAINT FK_E52C5D2BEF1A9D84');
$this->addSql('DROP SEQUENCE chill_main_notification_id_seq CASCADE');
$this->addSql('DROP TABLE chill_main_notification');
$this->addSql('DROP TABLE chill_main_notification_addresses_user');
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20210616134328 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code ADD origin INT DEFAULT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_postal_code DROP origin');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Address;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class HouseholdApiController extends ApiController
{
public function householdAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
}

View File

@@ -3,6 +3,7 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Form\HouseholdType;
use Chill\MainBundle\Entity\Address;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
@@ -104,6 +105,16 @@ class HouseholdController extends AbstractController
public function addresses(Request $request, Household $household)
{
// TODO ACL
//TODO put these lines into a validator constraint on household->getAddress
$addresses = $household->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
}
}
return $this->render('@ChillPerson/Household/addresses.html.twig',
[
'household' => $household
@@ -111,6 +122,7 @@ class HouseholdController extends AbstractController
);
}
/**
* @Route(
* "/{household_id}/address/move",
@@ -122,6 +134,7 @@ class HouseholdController extends AbstractController
public function addressMove(Request $request, Household $household)
{
// TODO ACL
return $this->render('@ChillPerson/Household/address_move.html.twig',
[
'household' => $household
@@ -177,4 +190,26 @@ class HouseholdController extends AbstractController
return $form;
}
/**
* @Route(
* "/{household_id}/address/edit",
* name="chill_person_household_address_edit",
* methods={"GET", "HEAD", "POST"}
* )
* @ParamConverter("household", options={"id" = "household_id"})
*/
public function addressEdit(Request $request, Household $household)
{
// TODO ACL
//$address = $this->findAddressById($household, $address_id); //TODO
return $this->render('@ChillPerson/Household/address_edit.html.twig',
[
'household' => $household,
//'address' => $address,
]
);
}
}

View File

@@ -45,7 +45,7 @@ class PersonAddressController extends AbstractController
* @var ValidatorInterface
*/
protected $validator;
/**
* PersonAddressController constructor.
*
@@ -55,7 +55,7 @@ class PersonAddressController extends AbstractController
{
$this->validator = $validator;
}
public function listAction($person_id)
{
$person = $this->getDoctrine()->getManager()

View File

@@ -22,7 +22,8 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Chill\MainBundle\Entity\Address;
class PersonApiController extends ApiController
{
@@ -35,7 +36,7 @@ class PersonApiController extends ApiController
{
$this->authorizationHelper = $authorizationHelper;
}
protected function createEntity(string $action, Request $request): object
{
$person = parent::createEntity($action, $request);
@@ -47,4 +48,10 @@ class PersonApiController extends ApiController
return $person;
}
public function personAddressApi($id, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, [ 'groups' => [ 'read' ] ]);
}
}

View File

@@ -201,7 +201,7 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'workflows' => [
'accompanying_period_lifecycle' => [
'type' => 'state_machine',
'audit_trail' => [
'audit_trail' => [
'enabled' => true
],
'marking_store' => [
@@ -354,15 +354,15 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'controller' => \Chill\PersonBundle\Controller\AccompanyingCourseApiController::class,
'actions' => [
'_entity' => [
'roles' => [
'roles' => [
Request::METHOD_GET => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PATCH => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_PUT => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
],
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
Request::METHOD_GET => true,
Request::METHOD_PUT => true,
Request::METHOD_PATCH => true,
]
],
'participation' => [
@@ -379,61 +379,61 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
'resource' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'comment' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'requestor' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'scope' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
],
'socialissue' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'socialIssueApi',
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
Request::METHOD_DELETE=> \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE
]
@@ -441,11 +441,11 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
'confirm' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_POST => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'roles' => [
'roles' => [
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter::SEE,
]
],
@@ -510,8 +510,48 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_HEAD => \Chill\PersonBundle\Security\Authorization\PersonVoter::SEE,
Request::METHOD_POST => \Chill\PersonBundle\Security\Authorization\PersonVoter::CREATE,
],
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'personAddressApi'
],
]
],
[
'class' => \Chill\PersonBundle\Entity\Household\Household::class,
'controller' => \Chill\PersonBundle\Controller\HouseholdApiController::class,
'name' => 'household',
'base_path' => '/api/1.0/person/household',
'base_role' => 'ROLE_USER',
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
'_entity' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
Request::METHOD_POST=> true,
]
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'householdAddressApi'
],
]
],
[

View File

@@ -7,6 +7,8 @@ use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
@@ -28,7 +30,7 @@ class Household
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
*/
private ?int $id = null;
@@ -40,6 +42,7 @@ class Household
* cascade={"persist", "remove", "merge", "detach"})
* @ORM\JoinTable(name="chill_person_household_to_addresses")
* @ORM\OrderBy({"validFrom" = "DESC"})
* @Serializer\Groups({"write"})
*/
private Collection $addresses;
@@ -48,7 +51,7 @@ class Household
* targetEntity=HouseholdMember::class,
* mappedBy="household"
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
*/
private Collection $members;
@@ -87,6 +90,11 @@ class Household
{
$this->addresses[] = $address;
foreach ($this->getAddresses() as $a) {
if ($a->getValidFrom() < $address->getValidFrom() && $a->getValidTo() === NULL) {
$a->setValidTo($address->getValidFrom());
}
}
return $this;
}
@@ -102,6 +110,7 @@ class Household
* By default, the addresses are ordered by date, descending (the most
* recent first)
*
* @Assert\Callback(methods={"validate"})
* @return \Chill\MainBundle\Entity\Address[]
*/
public function getAddresses()
@@ -294,4 +303,18 @@ class Household
return $this;
}
public function validate(ExecutionContextInterface $context, $payload)
{
$addresses = $this->getAddresses();
$cond = True;
for ($i=0; $i < count($addresses) - 1; $i++) {
if ($addresses[$i]->getValidFrom() != $addresses[$i + 1]->getValidTo()) {
$cond = False;
$context->buildViolation('The address are not sequentials. The validFrom date of one address should be equal to the validTo date of the previous address.')
->atPath('addresses')
->addViolation();
}
}
dump($cond);
}
}

View File

@@ -24,28 +24,17 @@ use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class AgeAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class AgeAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var
*/
protected $translator;
public function __construct($translator)
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function addRole()
{
return null;

View File

@@ -29,40 +29,23 @@ use Chill\MainBundle\Util\CountriesInfo;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CountryOfBirthAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var EntityRepository
*/
protected $countriesRepository;
private CountryRepository $countriesRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private TranslatableStringHelper $translatableStringHelper;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(EntityRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator)
{
public function __construct(
CountryRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->countriesRepository = $countriesRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
@@ -73,7 +56,6 @@ class CountryOfBirthAggregator implements AggregatorInterface,
return 'person';
}
public function buildForm(FormBuilderInterface $builder)
{
$builder->add('group_by_level', ChoiceType::class, array(

View File

@@ -26,31 +26,20 @@ use Symfony\Component\Translation\TranslatorInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Export\Declarations;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class GenderAggregator implements AggregatorInterface
final class GenderAggregator implements AggregatorInterface
{
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function applyOn()
{
return Declarations::PERSON_TYPE;
}
public function buildForm(FormBuilderInterface $builder)
{

View File

@@ -29,39 +29,23 @@ use Chill\MainBundle\Util\CountriesInfo;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\MainBundle\Export\ExportElementValidatedInterface;
use Chill\MainBundle\Repository\CountryRepository;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class NationalityAggregator implements AggregatorInterface,
ExportElementValidatedInterface
final class NationalityAggregator implements AggregatorInterface, ExportElementValidatedInterface
{
/**
*
* @var EntityRepository
*/
protected $countriesRepository;
private CountryRepository $countriesRepository;
/**
*
* @var TranslatableStringHelper
*/
protected $translatableStringHelper;
private TranslatableStringHelper $translatableStringHelper;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private TranslatorInterface $translator;
public function __construct(EntityRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator)
{
public function __construct(
CountryRepository $countriesRepository,
TranslatableStringHelper $translatableStringHelper,
TranslatorInterface $translator
) {
$this->countriesRepository = $countriesRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;

View File

@@ -0,0 +1,119 @@
<template>
<div class='household__address-move'>
<div class='household__address-move__create'>
<div>
<h2>{{ $t('create_a_new_address') }}</h2>
<add-address
@addNewAddress="addNewAddress">
</add-address>
</div>
<div>
<show-address
v-if="newAddress"
v-bind:address="newAddress">
</show-address>
</div>
</div>
<div class='household__address-move__valid'>
<h2>{{ $t('move_date') }}</h2>
<input
type="date"
name="validFrom"
:placeholder="$t('validFrom')"
v-model="validFrom"/>
<div v-if="errors.length > 0">
{{ errors }}
</div>
</div>
<div>
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a :href=backUrl class="sc-button bt-cancel">{{ $t('back_to_the_list') }}</a>
</li>
<li>
<button type="submit" class="sc-button bt-update centered" @click="addToHousehold">
{{ $t('add_an_address_to_household') }}
</button>
</li>
</ul>
</div>
</div>
</template>
<script>
import AddAddress from 'ChillMainAssets/vuejs/_components/AddAddress.vue';
import ShowAddress from 'ChillMainAssets/vuejs/_components/ShowAddress.vue';
export default {
name: 'App',
components: {
AddAddress,
ShowAddress
},
data() {
return {
edit: window.mode === 'edit',
householdId: window.householdId,
backUrl: `/fr/person/household/${householdId}/addresses`, //TODO better way to pass this
validFrom: new Date().toISOString().split('T')[0]
}
},
computed: {
newAddress() {
return this.$store.state.newAddress;
},
errors() {
return this.$store.state.errorMsg;
}
},
methods: {
addNewAddress({ address, modal }) {
console.log('@@@ CLICK button addNewAdress', address);
let createdAddress = {
'isNoAddress': address.isNoAddress,
'street': address.isNoAddress ? '' : address.street,
'streetNumber': address.isNoAddress ? '' : address.streetNumber,
'postcode': {'id': address.selected.city.id},
'floor': address.floor,
'corridor': address.corridor,
'steps': address.steps,
'flat': address.flat,
'buildingName': address.buildingName,
'distribution': address.distribution,
'extra': address.extra
};
if (address.selected.address.point !== undefined){
createdAddress = Object.assign(createdAddress, {
'point': address.selected.address.point.coordinates
});
}
if(address.writeNewPostalCode){
let newPostalCode = address.newPostalCode;
newPostalCode = Object.assign(newPostalCode, {
'country': {'id': address.selected.country.id },
});
createdAddress = Object.assign(createdAddress, {
'newPostalCode': newPostalCode
});
}
this.$store.dispatch('addAddress', createdAddress);
modal.showModal = false;
},
addToHousehold() {
this.$store.dispatch('addDateToAddressAndAddressToHousehold', {
householdId: this.householdId,
addressId: this.$store.state.newAddress.address_id,
body: { validFrom: {datetime: `${this.validFrom}T00:00:00+0100`}}
})
}
}
};
</script>

View File

@@ -0,0 +1,23 @@
/*
* Endpoint household
* method POST, post Household instance
*
* @id integer - id of household
* @body Object - dictionary with changes to post
*/
export const postAddressToHousehold = (householdId, addressId) => {
const body = {
'id': addressId
};
const url = `/api/1.0/person/household/${householdId}/address.json`
return fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json;charset=utf-8'},
body: JSON.stringify(body)
})
.then(response => {
if (response.ok) { return response.json(); }
throw Error('Error with request resource response');
});
};

View File

@@ -0,0 +1,47 @@
<template>
<div class="container">
<VueMultiselect
v-model="value"
@select="selectAddress"
name="field"
track-by="id"
label="value"
:custom-label="transName"
:multiple="false"
:placeholder="$t('select_address')"
:options="addresses">
</VueMultiselect>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
export default {
name: 'SelectHouseholdAddress',
components: { VueMultiselect },
props: ['address'],
data() {
return {
value: null
}
},
computed: {
addresses() {
return this.address.loaded.addresses;
}
},
methods: {
transName(value) {
return `${value.text} ${value.postcode.name}`
},
selectAddress(value) {
this.address.selected.address = value;
}
}
};
</script>
<style src="vue-multiselect/dist/vue-multiselect.css"></style>

View File

@@ -0,0 +1,25 @@
import { createApp } from 'vue'
import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'
import { appMessages } from './js/i18n'
import { store } from './store'
import App from './App.vue';
const root = window.vueRootComponent;
/*
* Load all App component, for Household edition page
*/
const i18n = _createI18n(appMessages);
const app = createApp({
template: `<app></app>`,
})
.use(store)
.use(i18n)
.component('app', App)
.mount('#household-address');

View File

@@ -0,0 +1,18 @@
import { addressMessages } from 'ChillMainAssets/vuejs/Address/js/i18n'
const appMessages = {
fr: {
select_a_existing_address: 'Sélectionner une adresse existante',
create_a_new_address: 'Créer une nouvelle adresse',
add_an_address_to_household: 'Déménager le ménage',
validFrom: 'Date du déménagement',
move_date: 'Date du déménagement',
back_to_the_list: 'Retour à la liste'
}
};
Object.assign(appMessages.fr, addressMessages.fr);
export {
appMessages
};

View File

@@ -0,0 +1,90 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { postAddress, postPostalCode, patchAddress } from 'ChillMainAssets/vuejs/_api/AddAddress';
import { postAddressToHousehold } from '../api';
const debug = process.env.NODE_ENV !== 'production';
const store = createStore({
strict: debug,
state: {
newAddress: {},
household: {},
validFrom: {},
errorMsg: []
},
getters: {
},
mutations: {
catchError(state, error) {
state.errorMsg.push(error);
},
addAddress(state, address) {
console.log('@M addAddress address', address);
state.newAddress = address;
},
addAddressToHousehold(state, household) {
console.log('@M addAddressToHousehold household', household);
state.household = household;
},
addDateToAddress(state, validFrom) {
console.log('@M addDateToAddress address.validFrom', validFrom);
state.validFrom = validFrom;
}
},
actions: {
addAddress({ commit }, payload) {
console.log('@A addAddress payload', payload);
if('newPostalCode' in payload){
postPostalCode(payload.newPostalCode)
.then(postalCode => {
let body = payload;
body.postcode = {'id': postalCode.id },
postAddress(body)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
})
} else {
postAddress(payload)
.then(address => new Promise((resolve, reject) => {
commit('addAddress', address);
resolve();
}))
.catch((error) => {
commit('catchError', error);
});
}
},
addDateToAddressAndAddressToHousehold({ commit }, payload) {
console.log('@A addDateToAddressAndAddressToHousehold payload', payload);
patchAddress(payload.addressId, payload.body)
.then(address => new Promise((resolve, reject) => {
commit('addDateToAddress', address.validFrom);
resolve();
}).then(
postAddressToHousehold(payload.householdId, payload.addressId)
.then(household => new Promise((resolve, reject) => {
commit('addAddressToHousehold', household);
resolve();
}))
.catch((error) => {
commit('catchError', error);
})
))
.catch((error) => {
commit('catchError', error);
});
},
}
});
export { store };

View File

@@ -18,31 +18,27 @@
{% set activeRouteKey = '' %}
{% block title 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
{% block title 'Modify address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) %}
{% block personcontent %}
<h1>{{ 'Update address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{% block content %}
<h1>{{ block('title') }}</h1>
<div id="address"></div>
{% endblock %}
{{ form_start(form) }}
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_row(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-save' }, 'label': 'Save' } ) }}
</li>
</ul>
{{ form_end(form) }}
{% block js %}
<script type="text/javascript">
window.personId = {{ person.id|e('js') }};
window.addressId = {{ address.id|e('js') }};
window.mode = 'edit';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('address') }}
{% endblock %}
{% endblock personcontent %}

View File

@@ -26,64 +26,76 @@
<h1>{{ 'Addresses\'history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
<table class="records_list">
<thead>
<tr>
<th>{{ 'Valid from'|trans }}</th>
<th>{{ 'Address'|trans }}</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
{% if person.addresses|length == 0 %}
<tr>
<td colspan="3">
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}">
<div class="person__address">
<div class="row">
<div class="person__address--date"></div>
<div class="person__address--content">
<div class="cell">
<div>
{% if person.addresses|length == 0 %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% endif %}
<a class="sc-button bt-create"
href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}">
{{ 'Add an address'|trans }}
</a>
</td>
</tr>
{% else %}
{% for address in person.addresses %}
<tr>
<td><strong>{{ 'Since %date%'|trans( { '%date%' : address.validFrom|format_date('long') } ) }}</strong></td>
</a>
</div>
</div>
</div>
</div>
<td>
{{ address_macros._render(address, { 'with_valid_from' : false, 'has_no_address': true } ) }}
</td>
{% for address in person.addresses %}
<div class="row">
<div class="person__address--date">
<div class="cell">
<div class="pill">
{% if address.validFrom is not empty %}
{{ address.validFrom|format_date('long') }}
{% endif %}
</div>
</div>
</div>
<div class="person__address--content">
<div class="cell">
<i class="dot"></i>
<div>
{% if address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% else %}
<div>
{% if address.street is not empty %}
<div class="street">
<i class="fa fa-fw fa-map-marker"></i>
<span class="streetNumber">{{ address.street }}</span>
{% if address.streetNumber is not empty %}
<span class="streetNumber">, {{ address.streetNumber }}</span>
{% endif %}
</div>
{% endif %}
{% if address.postCode is not empty %}
<div class="postCode">
<span>{{ address.postCode.code }}</span> <span>{{ address.postCode.name }}</span>
<span class="country">({{ address.postCode.country.name|localize_translatable_string }})</span>
</div>
{% endif %}
</div>
{% endif %}
<a href="{{ path('chill_person_address_edit', { 'person_id': person.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<td>
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_address_edit', { 'person_id': person.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</li>
</ul>
</td>
</tr>
<ul class="record_actions">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the person details'|trans }}
</a>
</li>
</ul>
{% endfor %}
{% endif %}
</tbody>
</table>
<ul class="record_actions">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the person details'|trans }}
</a>
</li>
<li>
<a href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}" class="sc-button bt-create">
{{ 'Add an address'|trans }}
</a>
</li>
</ul>
{% endblock personcontent %}

View File

@@ -22,36 +22,6 @@
{% block personcontent %}
<h1>{{ 'New address for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}</h1>
{{ form_start(form) }}
{{ form_row(form.isNoAddress) }}
{{ form_row(form.street) }}
{{ form_errors(form.street) }}
{{ form_row(form.streetNumber) }}
{{ form_errors(form.streetNumber) }}
{{ form_row(form.postCode) }}
{{ form_errors(form.postCode) }}
{{ form_row(form.validFrom) }}
{{ form_errors(form.validFrom) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_address_list', { 'person_id' : person.id } ) }}" class="sc-button bt-cancel">
{{ 'Back to the list'|trans }}
</a>
</li>
<li>
{{ form_row(form.submit, { 'attr' : { 'class': 'sc-button bt-create' }, 'label': 'Create' } ) }}
</li>
</ul>
{{ form_end(form) }}
NEW FORM
{% block content %}
<h1>{{ block('title') }}</h1>
<div id="address"></div>
@@ -62,6 +32,11 @@
{% endblock %}
{% block js %}
<script type="text/javascript">
window.personId = {{ person.id|e('js') }};
window.mode = 'new';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('address') }}
{% endblock %}

View File

@@ -0,0 +1,26 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'Edit household address'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<div>
<div id="household-address"></div>
</div>
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block js %}
<script type="text/javascript">
window.householdId = {{ household.id|e('js') }};
window.addressId = {{ address.id|e('js') }};
window.mode = 'edit';
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('household_address') }}
{% endblock %}
{% endblock %}

View File

@@ -3,9 +3,22 @@
{% block title 'Move household'|trans %}
{% block content %}
<h1>{{ block('title') }}</h1>
<h1>{{ block('title') }}</h1>
<p>Household with id {{ household.id }}</p>
<div>
<div id="household-address"></div>
</div>
{% block stylesheets %}
<link href="{{ asset('build/address.css') }}" type="text/css" rel="stylesheet" />
{% endblock %}
{% block js %}
<script type="text/javascript">
window.householdId = {{ household.id|e('js') }};
window.vueRootComponent = 'app';
</script>
{{ encore_entry_script_tags('household_address') }}
{% endblock %}
{% endblock %}

View File

@@ -4,12 +4,67 @@
{% block content %}
<h1>{{ block('title') }}</h1>
<div class="household">
<p>Household with id {{ household.id }}</p>
<div class="household__address">
<a class="sc-button bt-update"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
<div class="row">
<div class="household__address--date"></div>
<div class="household__address--content">
<div class="cell">
<a class="sc-button bt-create"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}
</a>
</div>
</div>
</div>
{% for address in household.addresses %}
<div class="row">
<div class="household__address--date">
<div class="cell">
<div class="pill">
{% if address.validFrom is not empty %}
{{ address.validFrom|format_date('long') }}
{% endif %}
</div>
</div>
</div>
<div class="household__address--content">
<div class="cell">
<i class="dot"></i>
<div>
{% if address.isNoAddress == true %}
<div class="chill_address_is_noaddress">{{ 'address.consider homeless'|trans }}</div>
{% else %}
<div>
{% if address.street is not empty %}
<div class="street">
<i class="fa fa-fw fa-map-marker"></i>
<span class="streetNumber">{{ address.street }}</span>
{% if address.streetNumber is not empty %}
<span class="streetNumber">, {{ address.streetNumber }}</span>
{% endif %}
</div>
{% endif %}
{% if address.postCode is not empty %}
<div class="postCode">
<span>{{ address.postCode.code }}</span> <span>{{ address.postCode.name }}</span>
<span class="country">({{ address.postCode.country.name|localize_translatable_string }})</span>
</div>
{% endif %}
</div>
{% endif %}
<a href="{{ path('chill_person_household_address_edit', { 'household_id': household.id, 'address_id' : address.id } ) }}" class="sc-button bt-edit"></a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -2,8 +2,8 @@
/*
* Chill is a software for social workers
*
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
*
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
* <http://www.champs-libres.coop>, <info@champs-libres.coop>
*
* This program is free software: you can redistribute it and/or modify
@@ -54,7 +54,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
{
static::bootKernel();
}
/**
* Setup before each test method (see phpunit doc)
*/
@@ -76,11 +76,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
$data = \json_decode($response->getContent());
$this->assertEquals($data->id, $period->getId(),
"test that the response's data contains the id of the period"
);
);
$this->assertGreaterThan(0, $data->participations);
}
@@ -106,7 +106,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
[],
\json_encode([ 'type' => 'social_issue', 'id' => $si->getId() ])
);
$this->assertEquals(200, $this->client->getResponse()->getStatusCode());
$data = \json_decode($this->client->getResponse()->getContent(), true);
@@ -204,7 +204,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$response = $this->client->getResponse();
$this->assertEquals(200, $response->getStatusCode());
// post a third party
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/resource.json', $period->getId()),
@@ -269,7 +269,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertInstanceOf(Person::class, $period->getRequestor());
$this->assertEquals($personId, $period->getRequestor()->getId());
// post a third party
// post a third party
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/requestor.json', $period->getId()),
@@ -332,7 +332,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$em = self::$container->get(EntityManagerInterface::class);
$this->client->request(
Request::METHOD_PATCH,
Request::METHOD_PATCH,
sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()),
[], // parameters
[], // files
@@ -368,7 +368,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -376,7 +376,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
ThirdParty::class." t ")
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($thirdPartyIds);
@@ -421,7 +421,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
// check by deownloading the accompanying cours
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
$response = $this->client->getResponse();
$data = \json_decode($response->getContent());
@@ -491,9 +491,9 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
{
// note about max result for person query, and maxGenerated:
//
//
// in the final loop, an id is popped out of the personIds array twice:
//
//
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
@@ -512,7 +512,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults($maxResults)
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -538,9 +538,9 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
public function dataGenerateRandomAccompanyingCourse()
{
// note about max result for person query, and maxGenerated:
//
//
// in the final loop, an id is popped out of the personIds array twice:
//
//
// * one for getting the person, which will in turn provide his accompanying period;
// * one for getting the personId to populate to the data manager
//
@@ -553,13 +553,26 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
$qb = $em->createQueryBuilder();
$personIds = $qb
->select('p.id')
->from(Person::class, 'p')
->where(
$qb->expr()->eq(
'p.center',
':center'
)
)
->andWhere(
$qb->expr()->gt(
'SIZE(p.accompanyingPeriodParticipations)',
0)
)
->setParameter('center', $center)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
// create a random order
shuffle($personIds);
@@ -597,7 +610,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->setParameter('center', $center)
->setMaxResults(100)
->getScalarResult();
// create a random order
shuffle($personIds);

View File

@@ -179,7 +179,7 @@ components:
readOnly: true
children_ids:
type: array
items:
items:
type: integer
readOnly: true
title:
@@ -210,7 +210,7 @@ components:
type: string
enum:
- 'household_position'
paths:
/1.0/person/person/{id}.json:
@@ -259,10 +259,48 @@ paths:
description: "Unauthorized"
422:
description: "Invalid data: the data is a valid json, could be deserialized, but does not pass validation"
/1.0/person/person/{id}/address.json:
post:
tags:
- person
summary: post an address to a person
parameters:
- name: id
in: path
required: true
description: The person id
schema:
type: integer
format: integer
minimum: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: The address id to attach to the person
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/person/social-work/social-issue.json:
get:
tags:
tags:
- social-issue
summary: Return a list of social work
responses:
@@ -270,7 +308,7 @@ paths:
description: "ok"
/1.0/person/social-work/social-issue/{id}.json:
get:
tags:
tags:
- social-issue
summary: Return a social issue by id
parameters:
@@ -424,7 +462,7 @@ paths:
description: "OK"
422:
description: "object with validation errors"
/1.0/person/accompanying-course/{id}/participation.json:
post:
tags:
@@ -784,6 +822,41 @@ paths:
400:
description: "transition cannot be applyed"
/1.0/person/household.json:
get:
tags:
- household
summary: Return a list of all household
responses:
200:
description: "ok"
/1.0/person/household/{id}.json:
get:
tags:
- household
summary: Return a household by id
parameters:
- name: id
in: path
required: true
description: The household id
schema:
type: integer
format: integer
minimum: 1
responses:
200:
description: "ok"
content:
application/json:
schema:
$ref: '#/components/schemas/Household'
404:
description: "not found"
401:
description: "Unauthorized"
/1.0/person/household/members/move.json:
post:
tags:
@@ -868,4 +941,39 @@ paths:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"
/1.0/person/household/{id}/address.json:
post:
tags:
- household
summary: post an address to a household
parameters:
- name: id
in: path
required: true
description: The household id
schema:
type: integer
format: integer
minimum: 1
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
id:
type: integer
description: The address id to attach to the household
responses:
401:
description: "Unauthorized"
404:
description: "Not found"
200:
description: "OK"
422:
description: "Unprocessable entity (validation errors)"
400:
description: "transition cannot be applyed"

View File

@@ -7,9 +7,10 @@ module.exports = function(encore, entries)
encore.addAliases({
ChillPersonAssets: __dirname + '/Resources/public'
});
encore.addEntry('accompanying_course', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('household_members_editor', __dirname + '/Resources/public/vuejs/HouseholdMembersEditor/index.js');
encore.addEntry('vue_accourse', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js');
encore.addEntry('household_edit_metadata', __dirname + '/Resources/public/modules/household_edit_metadata/index.js');
encore.addEntry('household_address', __dirname + '/Resources/public/vuejs/HouseholdAddress/index.js');
};

View File

@@ -73,6 +73,11 @@ services:
resource: '../Controller/'
tags: ['controller.service_arguments']
Chill\PersonBundle\Export\:
autowire: true
autoconfigure: true
resource: '../Export/'
Chill\PersonBundle\Templating\Entity\:
autowire: true
autoconfigure: true

View File

@@ -36,43 +36,39 @@ services:
chill.person.export.filter_birthdate:
class: Chill\PersonBundle\Export\Filter\BirthdateFilter
tags:
- { name: chill.export_filter, alias: person_birthdate_filter }
- { name: chill.export_filter, alias: person_birthdate_filter }
chill.person.export.filter_nationality:
class: Chill\PersonBundle\Export\Filter\NationalityFilter
arguments:
- "@chill.main.helper.translatable_string"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_filter, alias: person_nationality_filter }
chill.person.export.aggregator_nationality:
class: Chill\PersonBundle\Export\Aggregator\NationalityAggregator
arguments:
- "@chill.main.countries_repository"
- "@chill.main.helper.translatable_string"
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_nationality_aggregator }
chill.person.export.aggregator_country_of_birth:
class: Chill\PersonBundle\Export\Aggregator\CountryOfBirthAggregator
arguments:
- "@chill.main.countries_repository"
- "@chill.main.helper.translatable_string"
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_country_of_birth_aggregator }
chill.person.export.aggregator_gender:
class: Chill\PersonBundle\Export\Aggregator\GenderAggregator
arguments:
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_gender_aggregator }
chill.person.export.aggregator_age:
class: Chill\PersonBundle\Export\Aggregator\AgeAggregator
arguments:
- "@translator"
autowire: true
autoconfigure: true
tags:
- { name: chill.export_aggregator, alias: person_age_aggregator }

View File

@@ -187,6 +187,7 @@ Pick a person: Choisir une personne
No address given: Pas d'adresse renseignée
The address has been successfully updated: L'adresse a été mise à jour avec succès
Update address for %name%: Mettre à jour une adresse pour %name%
Modify address for %name%: Modifier une adresse pour %name%
Addresses'history for %name%: Historique des adresses de %name%
Addresses'history: Historique des adresses
New address for %name% : Nouvelle adresse pour %name%
@@ -319,3 +320,11 @@ Show Accompanying Course: Voir le parcours
Edit Accompanying Course: Modifier le parcours
Create Accompanying Course: Créer un nouveau parcours
Drop Accompanying Course: Supprimer le parcours
# Household
Household: Ménage
Summary: Résumé
Members: Membres
Addresses: Adresses
Move household: Nouveau déménagement
Addresses history for household: Historique des adresses