Merge remote-tracking branch 'origin/master' into HEAD

This commit is contained in:
2021-09-24 12:32:19 +02:00
273 changed files with 8744 additions and 3656 deletions

View File

@@ -73,7 +73,7 @@ class AccompanyingCourseController extends Controller
}
}
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $period);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
$em->persist($period);
$em->flush();
@@ -92,6 +92,8 @@ class AccompanyingCourseController extends Controller
*/
public function indexAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
// compute some warnings
// get persons without household
$withoutHousehold = [];
@@ -131,6 +133,8 @@ class AccompanyingCourseController extends Controller
*/
public function editAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/edit.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);
@@ -139,13 +143,15 @@ class AccompanyingCourseController extends Controller
/**
* History page of Accompanying Course section
*
* the page show anti chronologic history with all actions, title of page is 'accompanying course details'
* the page show anti chronologic history with all actions, title of page is 'Accompanying Course History'
*
* @Route("/{_locale}/parcours/{accompanying_period_id}/history", name="chill_person_accompanying_course_history")
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})
*/
public function historyAction(AccompanyingPeriod $accompanyingCourse): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
return $this->render('@ChillPerson/AccompanyingCourse/history.html.twig', [
'accompanyingCourse' => $accompanyingCourse
]);

View File

@@ -23,7 +23,10 @@
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Privacy\PrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Doctrine\DBAL\Exception;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\AccompanyingPeriodType;
@@ -53,21 +56,24 @@ class AccompanyingPeriodController extends AbstractController
*/
protected $validator;
/**
* AccompanyingPeriodController constructor.
*
* @param EventDispatcherInterface $eventDispatcher
* @param ValidatorInterface $validator
*/
public function __construct(EventDispatcherInterface $eventDispatcher, ValidatorInterface $validator)
{
protected AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
public function __construct(
AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository,
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator
) {
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
}
public function listAction(int $person_id): Response
/**
* @ParamConverter("person", options={"id"="person_id"})
*/
public function listAction(Person $person): Response
{
$person = $this->_getPerson($person_id);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $person);
$event = new PrivacyEvent($person, [
'element_class' => AccompanyingPeriod::class,
@@ -75,9 +81,10 @@ class AccompanyingPeriodController extends AbstractController
]);
$this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event);
$accompanyingPeriods = $person->getAccompanyingPeriodsOrdered();
$accompanyingPeriods = $this->accompanyingPeriodACLAwareRepository
->findByPerson($person, AccompanyingPeriodVoter::SEE);
return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', [
return $this->render('@ChillPerson/AccompanyingPeriod/list.html.twig', [
'accompanying_periods' => $accompanyingPeriods,
'person' => $person
]);

View File

@@ -230,13 +230,16 @@ final class PersonController extends AbstractController
*/
public function newAction(Request $request)
{
$defaultCenter = $this->security
->getUser()
->getGroupCenters()[0]
->getCenter();
$person = new Person();
$person = (new Person(new \DateTime('now')))
->setCenter($defaultCenter);
if (1 === count($this->security->getUser()
->getGroupCenters())) {
$person->setCenter(
$this->security->getUser()
->getGroupCenters()[0]
->getCenter()
);
}
$form = $this->createForm(CreationPersonType::class, $person, [
'validation_groups' => ['create']
@@ -244,7 +247,9 @@ final class PersonController extends AbstractController
'label' => 'Add the person'
])->add('createPeriod', SubmitType::class, [
'label' => 'Add the person and create an accompanying period'
]);
])->add('createHousehold', SubmitType::class, [
'label' => 'Add the person and create an household'
]); // TODO createHousehold form action
$form->handleRequest($request);

View File

@@ -24,8 +24,12 @@ namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Entity\Country;
use Chill\MainBundle\Entity\PostalCode;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CenterRepository;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Repository\ScopeRepository;
use Chill\MainBundle\Repository\UserRepository;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\MaritalStatus;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
@@ -90,12 +94,26 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
protected MaritalStatusRepository $maritalStatusRepository;
/**
* @var array|Scope[]
*/
protected array $cacheScopes = [];
protected ScopeRepository $scopeRepository;
/** @var array|User[] */
protected array $cacheUsers = [];
protected UserRepository $userRepository;
public function __construct(
Registry $workflowRegistry,
SocialIssueRepository $socialIssueRepository,
CenterRepository $centerRepository,
CountryRepository $countryRepository,
MaritalStatusRepository $maritalStatusRepository
MaritalStatusRepository $maritalStatusRepository,
ScopeRepository $scopeRepository,
UserRepository $userRepository
) {
$this->faker = Factory::create('fr_FR');
$this->faker->addProvider($this);
@@ -105,7 +123,8 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
$this->countryRepository = $countryRepository;
$this->maritalStatusRepository = $maritalStatusRepository;
$this->loader = new NativeLoader($this->faker);
$this->scopeRepository = $scopeRepository;
$this->userRepository = $userRepository;
}
public function getOrder()
@@ -220,10 +239,16 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
new \DateInterval('P' . \random_int(0, 180) . 'D')
)
);
$accompanyingPeriod->setCreatedBy($this->getRandomUser())
->setCreatedAt(new \DateTimeImmutable('now'));
$person->addAccompanyingPeriod($accompanyingPeriod);
$accompanyingPeriod->addSocialIssue($this->getRandomSocialIssue());
if (\random_int(0, 10) > 3) {
// always add social scope:
$accompanyingPeriod->addScope($this->getReference('scope_social'));
var_dump(count($accompanyingPeriod->getScopes()));
$accompanyingPeriod->setAddressLocation($this->createAddress());
$manager->persist($accompanyingPeriod->getAddressLocation());
$workflow = $this->workflowRegistry->get($accompanyingPeriod);
@@ -231,9 +256,19 @@ class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, Con
}
$manager->persist($person);
$manager->persist($accompanyingPeriod);
echo "add person'".$person->__toString()."'\n";
}
private function getRandomUser(): User
{
if (0 === count($this->cacheUsers)) {
$this->cacheUsers = $this->userRepository->findAll();
}
return $this->cacheUsers[\array_rand($this->cacheUsers)];
}
private function createAddress(): Address
{
$objectSet = $this->loader->loadData([

View File

@@ -40,13 +40,13 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
return 9600;
}
public function load(ObjectManager $manager)
{
foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) {
$permissionsGroup = $this->getReference($permissionsGroupRef);
$scopeSocial = $this->getReference('scope_social');
//create permission group
switch ($permissionsGroup->getName()) {
case 'social':
@@ -55,7 +55,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$permissionsGroup->addRoleScope(
(new RoleScope())
->setRole(AccompanyingPeriodVoter::SEE)
->setRole(AccompanyingPeriodVoter::FULL)
->setScope($scopeSocial)
);
@@ -87,7 +87,7 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($roleScopeUpdate);
$manager->persist($roleScopeCreate);
$manager->persist($roleScopeDuplicate);
break;
case 'administrative':
printf("Adding CHILL_PERSON_SEE to %s permission group \n", $permissionsGroup->getName());
@@ -98,9 +98,9 @@ class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface
$manager->persist($roleScopeSee);
break;
}
}
$manager->flush();
}

View File

@@ -18,6 +18,7 @@
namespace Chill\PersonBundle\DependencyInjection;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@@ -60,6 +61,9 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$container->setParameter('chill_person.allow_multiple_simultaneous_accompanying_periods',
$config['allow_multiple_simultaneous_accompanying_periods']);
// register all configuration in a unique parameter
$container->setParameter('chill_person', $config);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
$loader->load('services/widgets.yaml');
@@ -255,14 +259,26 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
*/
protected function prependRoleHierarchy(ContainerBuilder $container)
{
$container->prependExtensionConfig('security', array(
'role_hierarchy' => array(
'CHILL_PERSON_UPDATE' => array('CHILL_PERSON_SEE'),
'CHILL_PERSON_CREATE' => array('CHILL_PERSON_SEE'),
PersonVoter::LISTS => [ ChillExportVoter::EXPORT ],
PersonVoter::STATS => [ ChillExportVoter::EXPORT ]
)
));
$container->prependExtensionConfig('security', [
'role_hierarchy' => [
PersonVoter::UPDATE => [PersonVoter::SEE],
PersonVoter::CREATE => [PersonVoter::SEE],
PersonVoter::LISTS => [ChillExportVoter::EXPORT],
PersonVoter::STATS => [ChillExportVoter::EXPORT],
// accompanying period
AccompanyingPeriodVoter::SEE_DETAILS => [AccompanyingPeriodVoter::SEE],
AccompanyingPeriodVoter::CREATE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::DELETE => [AccompanyingPeriodVoter::SEE_DETAILS],
AccompanyingPeriodVoter::EDIT => [AccompanyingPeriodVoter::SEE_DETAILS],
// give all ACL for FULL
AccompanyingPeriodVoter::FULL => [
AccompanyingPeriodVoter::SEE_DETAILS,
AccompanyingPeriodVoter::CREATE,
AccompanyingPeriodVoter::EDIT,
AccompanyingPeriodVoter::DELETE
]
]
]);
}
/**

View File

@@ -43,23 +43,26 @@ class Configuration implements ConfigurationInterface
->arrayNode('validation')
->canBeDisabled()
->children()
->booleanNode('center_required')
->info('Enable a center for each person entity. If disabled, you must provide your own center provider')
->defaultValue(true)
->end()
->scalarNode('birthdate_not_after')
->info($this->validationBirthdateNotAfterInfos)
->defaultValue('P1D')
->validate()
->ifTrue(function($period) {
try {
$interval = new \DateInterval($period);
} catch (\Exception $ex) {
return true;
}
return false;
})
->thenInvalid('Invalid period for birthdate validation : "%s" '
. 'The parameter should match duration as defined by ISO8601 : '
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
->info($this->validationBirthdateNotAfterInfos)
->defaultValue('P1D')
->validate()
->ifTrue(function($period) {
try {
$interval = new \DateInterval($period);
} catch (\Exception $ex) {
return true;
}
return false;
})
->thenInvalid('Invalid period for birthdate validation : "%s" '
. 'The parameter should match duration as defined by ISO8601 : '
. 'https://en.wikipedia.org/wiki/ISO_8601#Durations')
->end() // birthdate_not_after, parent = children of validation
->end() // children for 'validation', parent = validation
->end() //validation, parent = children of root
->end() // children of root, parent = root

View File

@@ -24,6 +24,8 @@ namespace Chill\PersonBundle\Entity;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Entity\HasCentersInterface;
use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@@ -52,7 +54,8 @@ use Symfony\Component\Validator\Constraints as Assert;
* "accompanying_period"=AccompanyingPeriod::class
* })
*/
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface,
HasScopesInterface, HasCentersInterface
{
/**
* Mark an accompanying period as "occasional"
@@ -809,14 +812,21 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return $this;
}
/**
* @return iterable|Collection
*/
public function getScopes(): Collection
{
return $this->scopes;
}
public function addScope(Scope $scope): self
{
$this->scopes[] = $scope;
if (!$this->scopes->contains($scope)) {
$this->scopes[] = $scope;
}
return $this;
}
@@ -1040,4 +1050,16 @@ class AccompanyingPeriod implements TrackCreationInterface, TrackUpdateInterface
return 'none';
}
}
public function getCenters(): ?iterable
{
foreach ($this->getPersons() as $person) {
if (!in_array($person->getCenter(), $centers ?? [])
&& NULL !== $person->getCenter()) {
$centers[] = $person->getCenter();
}
}
return $centers ?? null;
}
}

View File

@@ -8,6 +8,29 @@ use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* This class links a person to the history of his addresses, through
* household membership.
*
* It is optimized on DB side, and compute the start date and end date
* of each address by the belonging of household.
*
* **note**: the start date and end date are the date of belonging to the address,
* not the belonging of the household.
*
* Example:
*
* * person A is member of household W from 2021-01-01 to 2021-12-01
* * person A is member of household V from 2021-12-01, still present after
* * household W lives in address Q from 2020-06-01 to 2021-06-01
* * household W lives in address R from 2021-06-01 to 2022-06-01
* * household V lives in address T from 2021-12-01 to still living there after
*
* The person A will have those 3 entities:
*
* 1. 1st entity: from 2021-01-01 to 2021-06-01, household W, address Q;
* 2. 2st entity: from 2021-06-01 to 2021-12-01, household W, address R;
* 3. 3st entity: from 2021-12-01 to NULL, household V, address T;
*
* @ORM\Entity(readOnly=true)
* @ORM\Table(name="view_chill_person_household_address")
*/
@@ -45,11 +68,23 @@ class PersonHouseholdAddress
*/
private $address;
/**
* The start date of the intersection address/household
*
* (this is not the startdate of the household, not
* the startdate of the address)
*/
public function getValidFrom(): ?\DateTimeInterface
{
return $this->validFrom;
}
/**
* The end date of the intersection address/household
*
* (this is not the enddate of the household, not
* the enddate of the address)
*/
public function getValidTo(): ?\DateTimeImmutable
{
return $this->validTo;

View File

@@ -35,6 +35,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\MainBundle\Entity\HasCenterInterface;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\Collection;
@@ -43,6 +44,11 @@ use Doctrine\Common\Collections\Criteria;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Symfony\Component\Validator\Constraints as Assert;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
/**
* Person Class
@@ -57,6 +63,12 @@ use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
* @DiscriminatorMap(typeProperty="type", mapping={
* "person"=Person::class
* })
* @PersonHasCenter(
* groups={"general", "creation"}
* )
* @HouseholdMembershipSequential(
* groups={"household_memberships"}
* )
*/
class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateInterface
{
@@ -75,6 +87,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank(
* groups={"general", "creation"}
* )
* @Assert\Length(
* max=255,
* groups={"general", "creation"}
* )
*/
private $firstName;
@@ -83,6 +102,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank(
* groups={"general", "creation"}
* )
* @Assert\Length(
* max=255,
* groups={"general", "creation"}
* )
*/
private $lastName;
@@ -102,6 +128,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
* @Birthdate(
* groups={"general", "creation"}
* )
*/
private $birthdate;
@@ -110,6 +142,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTimeImmutable
*
* @ORM\Column(type="date_immutable", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
*/
private ?\DateTimeImmutable $deathdate;
@@ -150,6 +185,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=9, nullable=true)
* @Assert\NotNull(
* groups={"general", "creation"}
* )
*/
private $gender;
@@ -179,8 +217,11 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var \DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Assert\Date(
* groups={"general", "creation"}
* )
*/
private $maritalStatusDate;
private ?\DateTime $maritalStatusDate;
/**
* Comment on marital status
@@ -202,6 +243,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", nullable=true)
* @Assert\Email(
* checkMX=true,
* groups={"general", "creation"}
* )
*/
private $email = '';
@@ -210,6 +255,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
* )
* @PhonenumberConstraint(
* type="landline",
* groups={"general", "creation"}
* )
*/
private $phonenumber = '';
@@ -218,6 +271,14 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="text", length=40, nullable=true)
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* groups={"general", "creation"}
* )
* @PhonenumberConstraint(
* type="mobile",
* groups={"general", "creation"}
* )
*/
private $mobilenumber = '';
@@ -230,12 +291,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* cascade={"persist", "remove", "merge", "detach"},
* orphanRemoval=true
* )
* @Assert\Valid(
* traverse=true,
* groups={"general", "creation"}
* )
*/
private $otherPhoneNumbers;
//TO-ADD caseOpeningDate
//TO-ADD nativeLanguag
/**
* The person's spoken languages
* @var ArrayCollection
@@ -254,7 +316,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var Center
*
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center")
* @ORM\JoinColumn(nullable=false)
*/
private $center;
@@ -353,6 +414,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private $addresses;
/**
* The current person address.
*
* This is computed through database and is optimized on database side.
*
* @var PersonCurrentAddress|null
* @ORM\OneToOne(targetEntity=PersonCurrentAddress::class, mappedBy="person")
*/
private ?PersonCurrentAddress $currentPersonAddress = null;
/**
* fullname canonical. Read-only field, which is calculated by
* the database.
* @var string
*
* @ORM\Column(type="text", nullable=true)
@@ -373,6 +446,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
private array $currentHouseholdAt = [];
/**
* Read-only field, computed by the database
*
* @ORM\OneToMany(
* targetEntity=PersonHouseholdAddress::class,
* mappedBy="person"
@@ -390,8 +465,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/**
* Person constructor.
*
* @param \DateTime|null $opening
*/
public function __construct()
{
@@ -404,6 +477,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
$this->householdAddresses = new ArrayCollection();
$this->genderComment = new CommentEmbeddable();
$this->maritalStatusComment = new CommentEmbeddable();
$this->periodLocatedOn = new ArrayCollection();
}
/**
@@ -501,6 +575,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $participation->getAccompanyingPeriod();
}
}
return null;
}
/**
@@ -1179,13 +1255,31 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->addresses;
}
/**
* @deprecated Use `getCurrentPersonAddress` instead
* @param DateTime|null $from
* @return false|mixed|null
* @throws \Exception
*/
public function getLastAddress(DateTime $from = null)
{
$from ??= new DateTime('now');
return $this->getCurrentPersonAddress($from);
}
/**
* get the address associated with the person at the given date
*
* @param DateTime|null $at
* @return Address|null
* @throws \Exception
*/
public function getCurrentPersonAddress(?\DateTime $at = null): ?Address
{
$at ??= new DateTime('now');
/** @var ArrayIterator $addressesIterator */
$addressesIterator = $this->getAddresses()
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $from)
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
->getIterator();
$addressesIterator->uasort(
@@ -1201,6 +1295,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* Validation callback that checks if the accompanying periods are valid
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"accompanying_period_consistent"}
* )
*/
public function isAccompanyingPeriodValid(ExecutionContextInterface $context)
{
@@ -1246,6 +1344,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* two addresses with the same validFrom date)
*
* This method add violation errors.
*
* @Assert\Callback(
* groups={"addresses_consistent"}
* )
*/
public function isAddressesValid(ExecutionContextInterface $context)
{
@@ -1425,7 +1527,16 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
public function getCurrentHouseholdAddress(?\DateTimeImmutable $at = null): ?Address
{
$at = $at === null ? new \DateTimeImmutable('today') : $at;
if (
NULL === $at
||
$at->format('Ymd') === (new \DateTime('today'))->format('Ymd')
) {
return $this->currentPersonAddress instanceof PersonCurrentAddress
? $this->currentPersonAddress->getAddress() : NULL;
}
// if not now, compute the date from history
$criteria = new Criteria();
$expr = Criteria::expr();

View File

@@ -0,0 +1,82 @@
<?php
namespace Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* Entity which associate person with his current address, through
* household participation.
*
* The computation is optimized on database side.
*
* The validFrom and validTo properties are the intersection of
* household membership and address validity. See @link{PersonHouseholdAddress}
*
* @ORM\Entity(readOnly=true)
* @ORM\Table("view_chill_person_current_address")
*/
class PersonCurrentAddress
{
/**
* @ORM\Id
* @ORM\OneToOne(targetEntity=Person::class, inversedBy="currentPersonAddress")
* @ORM\JoinColumn(name="person_id", referencedColumnName="id")
*/
protected Person $person;
/**
* @ORM\OneToOne(targetEntity=Address::class)
*/
protected Address $address;
/**
* @ORM\Column(name="valid_from", type="date_immutable")
*/
protected \DateTimeImmutable $validFrom;
/**
* @ORM\Column(name="valid_to", type="date_immutable")
*/
protected ?\DateTimeImmutable $validTo;
/**
* @return Person
*/
public function getPerson(): Person
{
return $this->person;
}
/**
* @return Address
*/
public function getAddress(): Address
{
return $this->address;
}
/**
* This date is the intersection of household membership
* and address validity
*
* @return \DateTimeImmutable
*/
public function getValidFrom(): \DateTimeImmutable
{
return $this->validFrom;
}
/**
* This date is the intersection of household membership
* and address validity
*
* @return \DateTimeImmutable|null
*/
public function getValidTo(): ?\DateTimeImmutable
{
return $this->validTo;
}
}

View File

@@ -21,7 +21,9 @@
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -33,6 +35,7 @@ use Chill\PersonBundle\Form\Type\GenderType;
use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\MainBundle\Form\Type\Export\PickCenterType;
final class CreationPersonType extends AbstractType
{
@@ -40,10 +43,6 @@ final class CreationPersonType extends AbstractType
// TODO: See if this is still valid and update accordingly.
const NAME = 'chill_personbundle_person_creation';
const FORM_NOT_REVIEWED = 'not_reviewed';
const FORM_REVIEWED = 'reviewed' ;
const FORM_BEING_REVIEWED = 'being_reviewed';
/**
*
* @var CenterTransformer
@@ -56,12 +55,16 @@ final class CreationPersonType extends AbstractType
*/
protected $configPersonAltNamesHelper;
private EventDispatcherInterface $dispatcher;
public function __construct(
CenterTransformer $centerTransformer,
ConfigPersonAltNamesHelper $configPersonAltNamesHelper
ConfigPersonAltNamesHelper $configPersonAltNamesHelper,
EventDispatcherInterface $dispatcher
) {
$this->centerTransformer = $centerTransformer;
$this->configPersonAltNamesHelper = $configPersonAltNamesHelper;
$this->dispatcher = $dispatcher;
}
/**
@@ -79,7 +82,9 @@ final class CreationPersonType extends AbstractType
->add('gender', GenderType::class, array(
'required' => true, 'placeholder' => null
))
->add('center', CenterType::class)
->add('center', CenterType::class, [
'required' => false
])
;
if ($this->configPersonAltNamesHelper->hasAltNames()) {
@@ -87,6 +92,11 @@ final class CreationPersonType extends AbstractType
'by_reference' => false
]);
}
$this->dispatcher->dispatch(
new CustomizeFormEvent(static::class, $builder),
CustomizeFormEvent::NAME
);
}
/**

View File

@@ -20,6 +20,9 @@
namespace Chill\PersonBundle\Form\Type;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\MaritalStatus;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer;
@@ -35,15 +38,13 @@ use Chill\MainBundle\Form\Type\Select2ChoiceType;
*/
class Select2MaritalStatusType extends AbstractType
{
/** @var RequestStack */
private $requestStack;
private EntityManagerInterface $em;
/** @var ObjectManager */
private $em;
private TranslatableStringHelper $translatableStringHelper;
public function __construct(RequestStack $requestStack,ObjectManager $em)
public function __construct(TranslatableStringHelper $translatableStringHelper, EntityManagerInterface $em)
{
$this->requestStack = $requestStack;
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
}
@@ -63,18 +64,17 @@ class Select2MaritalStatusType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$locale = $this->requestStack->getCurrentRequest()->getLocale();
$maritalStatuses = $this->em->getRepository('Chill\PersonBundle\Entity\MaritalStatus')->findAll();
$choices = array();
foreach ($maritalStatuses as $ms) {
$choices[$ms->getId()] = $ms->getName()[$locale];
$choices[$ms->getId()] = $this->translatableStringHelper->localize($ms->getName());
}
asort($choices, SORT_STRING | SORT_FLAG_CASE);
$resolver->setDefaults(array(
'class' => 'Chill\PersonBundle\Entity\MaritalStatus',
'class' => MaritalStatus::class,
'choices' => array_combine(array_values($choices),array_keys($choices))
));
}

View File

@@ -54,7 +54,7 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
return;
}
$menu->addChild($this->translator->trans('Accompanying Course Details'), [
$menu->addChild($this->translator->trans('Accompanying Course History'), [
'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [
'accompanying_period_id' => $period->getId()

View File

@@ -18,14 +18,17 @@
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Add menu entrie to person menu.
*
*
* Menu entries added :
*
*
* - person details ;
* - accompanying period (if `visible`)
*
@@ -37,21 +40,25 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
* @var string 'visible' or 'hidden'
*/
protected $showAccompanyingPeriod;
/**
*
* @var TranslatorInterface
*/
protected $translator;
private Security $security;
public function __construct(
$showAccompanyingPeriod,
ParameterBagInterface $parameterBag,
Security $security,
TranslatorInterface $translator
) {
$this->showAccompanyingPeriod = $showAccompanyingPeriod;
$this->showAccompanyingPeriod = $parameterBag->get('chill_person.accompanying_period');
$this->security = $security;
$this->translator = $translator;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
$menu->addChild($this->translator->trans('Person details'), [
@@ -83,8 +90,10 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
->setExtras([
'order' => 99999
]);
if ($this->showAccompanyingPeriod === 'visible') {
if ($this->showAccompanyingPeriod === 'visible'
&& $this->security->isGranted(AccompanyingPeriodVoter::SEE, $parameters['person'])
) {
$menu->addChild($this->translator->trans('Accompanying period list'), [
'route' => 'chill_person_accompanying_period_list',
'routeParameters' => [

View File

@@ -0,0 +1,76 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security;
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
{
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private Security $security;
private AuthorizationHelper $authorizationHelper;
private CenterResolverDispatcher $centerResolverDispatcher;
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository, Security $security, AuthorizationHelper $authorizationHelper, CenterResolverDispatcher $centerResolverDispatcher)
{
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->security = $security;
$this->authorizationHelper = $authorizationHelper;
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function findByPerson(
Person $person,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array {
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
$scopes = $this->authorizationHelper
->getReachableCircles($this->security->getUser(), $role,
$this->centerResolverDispatcher->resolveCenter($person));
if (0 === count($scopes)) {
return [];
}
$qb
->join('ap.participations', 'participation')
->where($qb->expr()->eq('participation.person', ':person'))
->andWhere(
$qb->expr()->orX(
'ap.confidential = FALSE',
$qb->expr()->eq('ap.user', ':user')
)
)
->andWhere(
$qb->expr()->orX(
$qb->expr()->neq('ap.step', ':draft'),
$qb->expr()->eq('ap.createdBy', ':creator')
)
)
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT)
->setParameter('person', $person)
->setParameter('user', $this->security->getUser())
->setParameter('creator', $this->security->getUser())
;
// add join condition for scopes
$orx = $qb->expr()->orX(
$qb->expr()->eq('ap.step', ':draft')
);
foreach ($scopes as $key => $scope) {
$orx->add($qb->expr()->isMemberOf(':scope_'.$key, 'ap.scopes'));
$qb->setParameter('scope_'.$key, $scope);
}
$qb->andWhere($orx);
return $qb->getQuery()->getResult();
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person;
interface AccompanyingPeriodACLAwareRepositoryInterface
{
public function findByPerson(
Person $person,
string $role,
?array $orderBy = [],
int $limit = null,
int $offset = null
): array;
}

View File

@@ -59,6 +59,11 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->findOneBy($criteria);
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
}
public function getClassName()
{
return AccompanyingPeriod::class;

View File

@@ -0,0 +1,292 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\CountryRepository;
use Chill\MainBundle\Search\ParsingException;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\NonUniqueResultException;
use Doctrine\ORM\NoResultException;
use Doctrine\ORM\Query;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Security\Core\Security;
final class PersonACLAwareRepository implements PersonACLAwareRepositoryInterface
{
private Security $security;
private EntityManagerInterface $em;
private CountryRepository $countryRepository;
private AuthorizationHelper $authorizationHelper;
public function __construct(
Security $security,
EntityManagerInterface $em,
CountryRepository $countryRepository,
AuthorizationHelper $authorizationHelper
) {
$this->security = $security;
$this->em = $em;
$this->countryRepository = $countryRepository;
$this->authorizationHelper = $authorizationHelper;
}
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getQueryResult($qb, 'p', $simplify, $limit, $start);
}
/**
* Helper method to prepare and return the search query for PersonACL.
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter. It also add query limits.
*
* The given alias must represent the person alias.
*
* @return array|Person[]
*/
public function getQueryResult(QueryBuilder $qb, string $alias, bool $simplify, int $limit, int $start): array
{
if ($simplify) {
$qb->select(
$alias.'.id',
$qb->expr()->concat(
$alias.'.firstName',
$qb->expr()->literal(' '),
$alias.'.lastName'
).'AS text'
);
} else {
$qb->select($alias);
}
$qb
->setMaxResults($limit)
->setFirstResult($start);
//order by firstname, lastname
$qb
->orderBy($alias.'.firstName')
->addOrderBy($alias.'.lastName');
if ($simplify) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
}
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): int {
$qb = $this->createSearchQuery($default, $firstname, $lastname,
$birthdate, $birthdateBefore, $birthdateAfter, $gender,
$countryCode);
$this->addACLClauses($qb, 'p');
return $this->getCountQueryResult($qb,'p');
}
/**
* Helper method to prepare and return the count for search query
*
* This method replace the select clause with required parameters, depending on the
* "simplify" parameter.
*
* The given alias must represent the person alias in the query builder.
*/
public function getCountQueryResult(QueryBuilder $qb, $alias): int
{
$qb->select('COUNT('.$alias.'.id)');
return $qb->getQuery()->getSingleScalarResult();
}
public function findBySimilaritySearch(string $pattern, int $firstResult,
int $maxResult, bool $simplify = false)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
return $this->getQueryResult($qb, 'sp', $simplify, $maxResult, $firstResult);
}
public function countBySimilaritySearch(string $pattern)
{
$qb = $this->createSimilarityQuery($pattern);
$this->addACLClauses($qb, 'sp');
return $this->getCountQueryResult($qb, 'sp');
}
/**
* Create a search query without ACL
*
* The person alias is a "p"
*
* @param string|null $default
* @param string|null $firstname
* @param string|null $lastname
* @param \DateTime|null $birthdate
* @param \DateTime|null $birthdateBefore
* @param \DateTime|null $birthdateAfter
* @param string|null $gender
* @param string|null $countryCode
* @return QueryBuilder
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function createSearchQuery(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): QueryBuilder {
if (!$this->security->getUser() instanceof User) {
throw new \RuntimeException("Search must be performed by a valid user");
}
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'p');
if (NULL !== $firstname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
->setParameter('firstname', '%'.$firstname.'%');
}
if (NULL !== $lastname) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
->setParameter('lastname', '%'.$lastname.'%');
}
if (NULL !== $birthdate) {
$qb->andWhere($qb->expr()->eq('s.birthdate', ':birthdate'))
->setParameter('birthdate', $birthdate);
}
if (NULL !== $birthdateAfter) {
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
->setParameter('birthdateafter', $birthdateAfter);
}
if (NULL !== $birthdateBefore) {
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
->setParameter('birthdatebefore', $birthdateBefore);
}
if (NULL !== $gender) {
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
->setParameter('gender', $gender);
}
if (NULL !== $countryCode) {
try {
$country = $this->countryRepository->findOneBy(['countryCode' => $countryCode]);
} catch (NoResultException $ex) {
throw new ParsingException('The country code "'.$countryCode.'" '
. ', used in nationality, is unknow', 0, $ex);
} catch (NonUniqueResultException $e) {
throw $e;
}
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
->setParameter('nationality', $country);
}
if (NULL !== $default) {
$grams = explode(' ', $default);
foreach($grams as $key => $gram) {
$qb->andWhere($qb->expr()
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
->setParameter('default_'.$key, '%'.$gram.'%');
}
}
return $qb;
}
private function addACLClauses(QueryBuilder $qb, string $personAlias): void
{
// restrict center for security
$reachableCenters = $this->authorizationHelper
->getReachableCenters($this->security->getUser(), 'CHILL_PERSON_SEE');
$qb->andWhere(
$qb->expr()->orX(
$qb->expr()
->in($personAlias.'.center', ':centers'),
$qb->expr()
->isNull($personAlias.'.center')
)
);
$qb->setParameter('centers', $reachableCenters);
}
/**
* Create a query for searching by similarity.
*
* The person alias is "sp".
*
* @param $pattern
* @return QueryBuilder
*/
public function createSimilarityQuery($pattern): QueryBuilder
{
$qb = $this->em->createQueryBuilder();
$qb->from(Person::class, 'sp');
$grams = explode(' ', $pattern);
foreach($grams as $key => $gram) {
$qb->andWhere('STRICT_WORD_SIMILARITY_OPS(:default_'.$key.', sp.fullnameCanonical) = TRUE')
->setParameter('default_'.$key, '%'.$gram.'%');
// remove the perfect matches
$qb->andWhere($qb->expr()
->notLike('sp.fullnameCanonical', 'UNACCENT(LOWER(:not_default_'.$key.'))'))
->setParameter('not_default_'.$key, '%'.$gram.'%');
}
return $qb;
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Search\ParsingException;
use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\NonUniqueResultException;
interface PersonACLAwareRepositoryInterface
{
/**
* @return array|Person[]
* @throws NonUniqueResultException
* @throws ParsingException
*/
public function findBySearchCriteria(
int $start,
int $limit,
bool $simplify = false,
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
): array;
public function countBySearchCriteria(
string $default = null,
string $firstname = null,
string $lastname = null,
?\DateTime $birthdate = null,
?\DateTime $birthdateBefore = null,
?\DateTime $birthdateAfter = null,
string $gender = null,
string $countryCode = null
);
public function findBySimilaritySearch(
string $pattern,
int $firstResult,
int $maxResult,
bool $simplify = false
);
public function countBySimilaritySearch(string $pattern);
}

View File

@@ -23,9 +23,11 @@ use Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
use UnexpectedValueException;
final class PersonRepository
final class PersonRepository implements ObjectRepository
{
private EntityRepository $repository;
@@ -44,6 +46,26 @@ final class PersonRepository
return $this->repository->findBy(['id' => $ids]);
}
public function findAll()
{
return $this->repository->findAll();
}
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null)
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria)
{
return $this->repository->findOneBy($criteria);
}
public function getClassName()
{
return Person::class;
}
/**
* @param $centers
* @param $firstResult

View File

@@ -87,7 +87,6 @@ div.person-view {
}
}
/*
* ACCOMPANYING_COURSE CONTEXT
* Header custom for Accompanying Course
@@ -125,7 +124,6 @@ abbr.referrer {
align-self: center; // in flex context
}
/*
* HOUSEHOLD CONTEXT
* Header custom for Household
@@ -157,12 +155,19 @@ div.banner {
span.badge-member {
flex-shrink: 0; flex-grow: 0; flex-basis: auto;
color: $white;
border: 1px solid #ffffff3b;
border: 1px solid transparentize($white, 0.75);
border-bottom: 3px solid transparentize( shade-color( $chill-green, 20%), 0.3);
border-radius: 8px;
padding: 0.4em 0.8em;
padding: 0.2em 0.7em;
margin-bottom: 0.2em;
margin-right: 0.3em;
&.holder { order: -1; }
&.holder {
order: -1;
.fa-holder .text-success {
color: transparentize( shade-color( $chill-green, 20%), 0.3) !important;
}
}
&.child { order: 2; }
}
}
@@ -179,3 +184,66 @@ div.banner {
}
}
}
div.household-resume {
display: flex;
flex-direction: row;
align-items: center;
div.col-address {
font-size: 120%;
padding-left: 1em;
}
div.col-comment {
//padding: 0;
margin-bottom: 1rem;
display: flex;
flex-direction: column;
> * > * {
& > .chill-user-quote {
margin: 1.5em -1.67em 0;
}
}
}
}
/*
* BADGES, MARKS, PINS
* for chill person theme
*/
// chill person badges
span.badge-person,
span.badge-thirdparty {
display: inline-block;
padding: 0 0.5em !important;
background-color: $white;
color: $dark;
border: 1px solid $chill-ll-gray;
border-bottom-width: 2px;
border-bottom-style: solid;
border-radius: 6px;
a {
text-decoration: none;
}
}
span.badge-person {
border-bottom-color: $chill-green;
}
// todo: move in thirdparty
span.badge-thirdparty {
border-bottom-color: shade-color($chill-pink, 10%);
}
// household holder mark
span.fa-holder {
width: 1em;
margin: -10px 0.3em -8px 0;
i:last-child {
font-weight: 900;
color: white;
font-size: 70%;
font-family: "Open Sans Extrabold";
}
}

View File

@@ -26,26 +26,3 @@
}
}
}
// specific chill badges
span.badge-person,
span.badge-thirdparty {
display: inline-block;
padding: 0 0.5em !important;
background-color: $white;
color: $dark;
border: 1px solid $chill-ll-gray;
border-bottom-width: 2px;
border-bottom-style: solid;
border-radius: 6px;
a {
text-decoration: none;
}
}
span.badge-person {
border-bottom-color: $chill-green;
}
// todo: move in thirdparty
span.badge-thirdparty {
border-bottom-color: shade-color($chill-pink, 10%);
}

View File

@@ -10,6 +10,7 @@
<origin-demand></origin-demand>
<requestor></requestor>
<social-issue></social-issue>
<scopes></scopes>
<referrer></referrer>
<resources></resources>
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
@@ -32,6 +33,7 @@ import PersonsAssociated from './components/PersonsAssociated.vue';
import Requestor from './components/Requestor.vue';
import SocialIssue from './components/SocialIssue.vue';
import CourseLocation from './components/CourseLocation.vue';
import Scopes from './components/Scopes.vue';
import Referrer from './components/Referrer.vue';
import Resources from './components/Resources.vue';
import Comment from './components/Comment.vue';
@@ -47,6 +49,7 @@ export default {
Requestor,
SocialIssue,
CourseLocation,
Scopes,
Referrer,
Resources,
Comment,
@@ -77,7 +80,7 @@ export default {
left: -22px;
top: 4px;
}
a[name^="section"] {
a[id^="section"] {
position: absolute;
top: -2.5em; // reference for stickNav
}
@@ -93,7 +96,8 @@ export default {
}
& > div {
margin: 1em 3em 0;
&.flex-table {
&.flex-table,
&.flex-bloc {
margin: 1em 0 0;
}
}

View File

@@ -191,7 +191,49 @@ const getListOrigins = () => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while retriving origin\'s list.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
}
};
const addScope = (id, scope) => {
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
console.log(url);
console.log(scope);
return fetch(url, {
method: 'POST',
body: JSON.stringify({
id: scope.id,
type: scope.type,
}),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
})
.then(response => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
};
const removeScope = (id, scope) => {
const url = `/api/1.0/person/accompanying-course/${id}/scope.json`;
console.log(url);
console.log(scope);
return fetch(url, {
method: 'DELETE',
body: JSON.stringify({
id: scope.id,
type: scope.type,
}),
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
})
.then(response => {
if (response.ok) { return response.json(); }
throw { msg: 'Error while adding scope', sta: response.status, txt: response.statusText, err: new Error(), body: response.body };
});
};
export {
getAccompanyingCourse,
@@ -204,5 +246,7 @@ export {
getUsers,
whoami,
getListOrigins,
postSocialIssue
postSocialIssue,
addScope,
removeScope,
};

View File

@@ -13,7 +13,7 @@
<h2 class="modal-title">{{ $t('courselocation.sure') }}</h2>
</template>
<template v-slot:body>
<show-address :address="person.current_household_address"></show-address>
<address-render-box :address="person.current_household_address"></address-render-box>
<p>{{ $t('courselocation.sure_description') }}</p>
</template>
<template v-slot:footer>
@@ -28,12 +28,12 @@
<script>
import {mapState} from "vuex";
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import ShowAddress from "ChillMainAssets/vuejs/Address/components/ShowAddress";
import AddressRenderBox from "ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue";
export default {
name: "ButtonLocation",
components: {
ShowAddress,
AddressRenderBox,
Modal,
},
props: ['person'],
@@ -54,8 +54,8 @@ export default {
assignAddress() {
//console.log('assignAddress id', this.person.current_household_address);
let payload = {
entity: this.context.entity.type,
entityId: this.context.entity.id,
target: this.context.target.name,
targetId: this.context.target.id,
locationStatusTo: 'person',
personId: this.person.id
};

View File

@@ -1,6 +1,6 @@
<template>
<div class="vue-component">
<h2><a name="section-80"></a>{{ $t('comment.title') }}</h2>
<h2><a id="section-90"></a>{{ $t('comment.title') }}</h2>
<!--div class="error flash_message" v-if="errors.length > 0">
{{ errors[0] }}

View File

@@ -1,6 +1,6 @@
<template>
<div class="vue-component">
<h2><a name="section-90"></a>
<h2><a id="section-100"></a>
{{ $t('confirm.title') }}
</h2>
<div>
@@ -88,6 +88,10 @@ export default {
socialIssue: {
msg: 'confirm.socialIssue_not_valid',
anchor: '#section-50'
},
scopes: {
msg: 'confirm.set_a_scope',
anchor: '#section-65'
}
}
}

View File

@@ -1,10 +1,9 @@
<template>
<div class="vue-component">
<h2><a name="section-20"></a>
<h2><a id="section-20"></a>
{{ $t('courselocation.title') }}
</h2>
<!-- {# include vue_address component #} -->
<div v-for="error in displayErrors" class="alert alert-danger my-2">
{{ error }}
</div>
@@ -17,9 +16,9 @@
<div class="flex-table" v-if="accompanyingCourse.location">
<div class="item-bloc">
<show-address
<address-render-box
:address="accompanyingCourse.location">
</show-address>
</address-render-box>
<div v-if="isPersonLocation" class="alert alert-secondary separator">
<label class="col-form-label">
@@ -41,8 +40,7 @@
:context="context"
:key="addAddress.type"
:options="addAddress.options"
:result="addAddress.result"
@submitAddress="submitTemporaryAddress"
:addressChangedCallback="submitTemporaryAddress"
ref="addAddress">
</add-address>
</li>
@@ -63,13 +61,13 @@
<script>
import { mapState } from "vuex";
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
import ShowAddress from 'ChillMainAssets/vuejs/Address/components/ShowAddress.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
export default {
name: "CourseLocation",
components: {
AddAddress,
ShowAddress
AddressRenderBox
},
data() {
return {
@@ -89,10 +87,12 @@ export default {
edit: 'courselocation.edit_temporary_address'
},
/// Display each step in page or Modal
bindModal: {
//step1: false, step2: false
},
hideDateFrom: true
openPanesInModal: true,
// Use Date fields
//useDate: {
// validFrom: true
//},
hideAddress: true
}
}
}
@@ -118,8 +118,8 @@ export default {
methods: {
initAddressContext() {
let context = {
entity: {
type: this.accompanyingCourse.type,
target: {
name: this.accompanyingCourse.type,
id: this.accompanyingCourse.id
},
edit: false,
@@ -132,31 +132,30 @@ export default {
this.$store.commit('setAddressContext', context);
},
removeAddress() {
//console.log('remove address');
let payload = {
entity: this.context.entity.type,
entityId: this.context.entity.id,
target: this.context.target.name,
targetId: this.context.target.id,
locationStatusTo: 'none'
};
//console.log('remove address');
this.$store.dispatch('updateLocation', payload);
},
displayErrors() {
return this.$refs.addAddress.errorMsg;
},
submitTemporaryAddress() {
//console.log('@@@ click on Submit Temporary Address Button');
let payload = this.$refs.addAddress.submitNewAddress();
submitTemporaryAddress(payload) {
//console.log('@@@ click on Submit Temporary Address Button', payload);
payload['locationStatusTo'] = 'address'; // <== temporary, not none, not person
this.$store.dispatch('updateLocation', payload);
this.$store.commit('setEditContextTrue');
this.$store.commit('setEditContextTrue', payload);
}
},
mounted() {
created() {
this.initAddressContext();
//console.log('ac.locationStatus', this.accompanyingCourse.locationStatus);
//console.log('ac.location (temporary location)', this.accompanyingCourse.location);
//console.log('ac.personLocation', this.accompanyingCourse.personLocation);
console.log('ac.locationStatus', this.accompanyingCourse.locationStatus);
console.log('ac.location (temporary location)', this.accompanyingCourse.location);
console.log('ac.personLocation', this.accompanyingCourse.personLocation);
}
}
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="vue-component">
<h2><a name="section-30"></a>{{ $t('origin.title') }}</h2>
<h2><a id="section-30"></a>{{ $t('origin.title') }}</h2>
<div class="mb-4">
<label for="selectOrigin">

View File

@@ -1,17 +1,45 @@
<template>
<div class="vue-component">
<h2><a name="section-10"></a>{{ $t('persons_associated.title')}}</h2>
<h2><a id="section-10"></a>{{ $t('persons_associated.title')}}</h2>
<div v-if="participations.length > 0">
<div v-if="currentParticipations.length > 0">
<label class="col-form-label">{{ $tc('persons_associated.counter', counter) }}</label>
</div>
<div v-else>
<label class="chill-no-data-statement">{{ $tc('persons_associated.counter', counter) }}</label>
</div>
<div v-if="participationWithoutHousehold.length > 0" class="alert alert-warning no-household">
<i class="fa fa-warning fa-2x"></i>
<form method="GET" action="/fr/person/household/members/editor">
<div class="float-button bottom"><div class="box">
<div class="action">
<button class="btn btn-update" type="submit">{{ $t('persons_associated.update_household') }}</button>
</div>
<p class="mb-3">{{ $t('persons_associated.person_without_household_warning') }}</p>
<div class="form-check" v-for="p in participationWithoutHousehold">
<input type="checkbox"
class="form-check-input"
v-model="hasNoHousehold"
name="persons[]"
checked="checked"
:id="p.person.id"
:value="p.person.id"
/>
<label class="form-check-label" for="hasNoHousehold">
{{ p.person.text }}
</label>
</div>
<input type="hidden" name="expand_suggestions" value="true">
<input type="hidden" name="returnPath" :value="getReturnPath">
<input type="hidden" name="accompanying_period_id" :value="courseId">
</div></div>
</form>
</div>
<div class="flex-table mb-3">
<participation-item
v-for="participation in participations"
v-for="participation in currentParticipations"
v-bind:participation="participation"
v-bind:key="participation.id"
@remove="removeParticipation"
@@ -56,10 +84,24 @@ export default {
}
}
},
computed: mapState({
participations: state => state.accompanyingCourse.participations,
counter: state => state.accompanyingCourse.participations.length
}),
computed: {
...mapState({
courseId: state => state.accompanyingCourse.id,
participations: state => state.accompanyingCourse.participations
}),
currentParticipations() {
return this.participations.filter(p => p.endDate === null)
},
counter() {
return this.currentParticipations.length;
},
participationWithoutHousehold() {
return this.currentParticipations.filter(p => p.person.current_household_id === null);
},
getReturnPath() {
return window.location.pathname + window.location.search + window.location.hash;
}
},
methods: {
removeParticipation(item) {
//console.log('@@ CLICK remove participation: item', item);
@@ -81,3 +123,30 @@ export default {
}
}
</script>
<style lang="scss" scoped>
div#accompanying-course {
div.vue-component {
& > div.alert {
margin: 0 0 -1em;
}
div.no-household {
padding-bottom: 1.5em;
display: flex;
flex-direction: row;
& > i {
flex-basis: 1.5em; flex-grow: 0; flex-shrink: 0;
padding-top: 0.2em;
opacity: 0.75;
}
& > form {
flex-basis: auto;
div.action {
button.btn-update {
margin-right: 2em;
}
}
}
}
}
}
</style>

View File

@@ -1,15 +1,17 @@
<template>
<person-render-box
<person-render-box render="bloc"
:options="{
addInfo : true,
addId : false,
addEntity: false,
addLink: false,
addHouseholdLink: true,
addAltNames: true,
addAge : false,
addAge : true,
hLevel : 3,
}"
:person="participation.person">
:person="participation.person"
:returnPath="getAccompanyingCourseReturnPath">
<template v-slot:record-actions>
<ul class="record_actions">
@@ -40,34 +42,54 @@
<li>
<button v-if="!participation.endDate"
class="btn btn-sm btn-remove"
v-bind:title="$t('action.remove')"
@click.prevent="$emit('close', participation)">
v-bind:title="$t('persons_associated.leave_course')"
@click="modal.showModal = true">
</button>
<button v-else
class="btn btn-sm btn-remove disabled"></button>
</li>
</ul>
</template>
</person-render-box>
<teleport to="body">
<modal v-if="modal.showModal" :modalDialogClass="modal.modalDialogClass" @close="modal.showModal = false">
<template v-slot:header>
<h2 class="modal-title">{{ $t('persons_associated.sure') }}</h2>
</template>
<template v-slot:body>
<p>{{ $t('persons_associated.sure_description') }}</p>
</template>
<template v-slot:footer>
<button class="btn btn-danger" @click.prevent="$emit('close', participation)">
{{ $t('persons_associated.ok') }}
</button>
</template>
</modal>
</teleport>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/_components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
export default {
name: 'ParticipationItem',
components: {
OnTheFly,
ButtonLocation,
PersonRenderBox
PersonRenderBox,
Modal
},
props: ['participation'],
emits: ['remove', 'close'],
data() {
return {
modal: {
showModal: false,
modalDialogClass: "modal-dialog-centered modal-md"
},
PersonRenderBox: {
participation: 'participation',
options: {
@@ -86,6 +108,9 @@ export default {
return true;
}
return false;
},
getAccompanyingCourseReturnPath() {
return `fr/parcours/${this.$store.state.accompanyingCourse.id}/edit#section-10`;
}
}
}
@@ -93,10 +118,7 @@ export default {
/*
* dates of participation
*
* :title="$t('persons_associated.date_start_to_end', {
* start: $d(participation.startDate.datetime, 'short'),
* end: $d(participation.endDate.datetime, 'short')
* })"
*
*
* <tr>
* <td><span v-if="participation.startDate">

View File

@@ -1,6 +1,6 @@
<template>
<div class="vue-component">
<h2><a name="section-60"></a>{{ $t('referrer.title') }}</h2>
<h2><a id="section-70"></a>{{ $t('referrer.title') }}</h2>
<div>
<label class="col-form-label" for="selectReferrer">

View File

@@ -1,7 +1,7 @@
<template>
<div class="vue-component">
<h2><a name="section-40"></a>{{ $t('requestor.title') }}</h2>
<h2><a id="section-40"></a>{{ $t('requestor.title') }}</h2>
<div v-if="accompanyingCourse.requestor" class="flex-table">
@@ -16,7 +16,7 @@
addLink: false,
addId: false,
addEntity: true,
addInfo: true,
addInfo: false,
hLevel: 3,
isMultiline: true
}"
@@ -30,7 +30,7 @@
</template>
</third-party-render-box>
<person-render-box v-else-if="accompanyingCourse.requestor.type == 'person'"
<person-render-box render="bloc" v-else-if="accompanyingCourse.requestor.type == 'person'"
:person="accompanyingCourse.requestor"
:options="{
addLink: false,

View File

@@ -1,7 +1,7 @@
<template>
<div class="vue-component">
<h2><a name="section-70"></a>{{ $t('resources.title')}}</h2>
<h2><a id="section-80"></a>{{ $t('resources.title')}}</h2>
<div v-if="resources.length > 0">
<label class="col-form-label">{{ $tc('resources.counter', counter) }}</label>
@@ -10,7 +10,7 @@
<label class="chill-no-data-statement">{{ $tc('resources.counter', counter) }}</label>
</div>
<div class="flex-table mb-3">
<div class="flex-bloc mb-3">
<resource-item
v-for="resource in resources"
v-bind:resource="resource"
@@ -76,3 +76,11 @@ export default {
}
}
</script>
<style lang="scss">
div.flex-bloc {
div.item-bloc {
flex-basis: 50%;
}
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<person-render-box
<person-render-box render="bloc"
v-if="resource.resource.type === 'person'"
:person="resource.resource"
:options="{ addInfo : true, addId : false, addEntity: true, addLink: false, addAltNames: true, addAge : false, hLevel : 3 }"
@@ -17,7 +17,7 @@
<third-party-render-box
v-if="resource.resource.type === 'thirdparty'"
:thirdparty="resource.resource"
:options="{ addLink : false, addId : false, addEntity: true, addInfo: true, hLevel: 3 }"
:options="{ addLink : false, addId : false, addEntity: true, addInfo: false, hLevel: 3 }"
>
<template v-slot:record-actions>
<ul class="record_actions">

View File

@@ -0,0 +1,47 @@
<template>
<div class="vue-component">
<h2><a id="section-60"></a>{{ $t('scopes.title') }}</h2>
<ul>
<li v-for="s in scopes">
<input type="checkbox" v-model="checkedScopes" :value="s" />
{{ s.name.fr }}
</li>
</ul>
<div v-if="!isScopeValid" class="alert alert-warning separator">
{{ $t('scopes.add_at_least_one') }}
</div>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
export default {
name: "Scopes",
computed: {
...mapState([
'scopes',
'scopesAtStart'
]),
...mapGetters([
'isScopeValid'
]),
checkedScopes: {
get: function() {
return this.$store.state.accompanyingCourse.scopes;
},
set: function(v) {
this.$store.dispatch('setScopes', v);
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -1,6 +1,6 @@
<template>
<div class="vue-component">
<h2><a name="section-50"></a>{{ $t('social_issue.title') }}</h2>
<h2><a id="section-50"></a>{{ $t('social_issue.title') }}</h2>
<div class="my-4">
<!--label for="field">{{ $t('social_issue.label') }}</label
@@ -78,6 +78,7 @@ export default {
@import 'ChillMainAssets/module/bootstrap/shared';
@import 'ChillPersonAssets/chill/scss/mixins';
div#accompanying-course {
span.multiselect__tag {
@include badge_social_issue;
background: $chill-l-gray;
@@ -93,6 +94,7 @@ export default {
}
}
}
}
</style>

View File

@@ -62,42 +62,42 @@ export default {
// load datas DOM when mounted ready
this.container = document.querySelector("#content");
this.stickyNav = document.querySelector('#navmap');
this.anchors = document.querySelectorAll("h2 a[name^='section']");
this.initItemsMap();
this.anchors = document.querySelectorAll("h2 a[id^='section']");
this.initItemsMap();
// TODO resizeObserver not supports IE !
// Listen when elements change size, then recalculate heightSum and initItemsMap
const resizeObserver = new ResizeObserver(entries => {
this.refreshPos();
});
resizeObserver.observe(this.header);
resizeObserver.observe(this.bannerName);
resizeObserver.observe(this.bannerDetails);
resizeObserver.observe(this.container);
},
initItemsMap() {
this.anchors.forEach(anchor => {
this.items.push({
pos: null,
active: false,
key: parseInt(anchor.name.slice(8).slice(0, -1)),
name: '#' + anchor.name
key: parseInt(anchor.id.slice(8).slice(0, -1)),
id: '#' + anchor.id
})
});
},
refreshPos() {
//console.log('refreshPos');
this.heightSum = this.header.offsetHeight + this.bannerName.offsetHeight + this.bannerDetails.offsetHeight;
this.anchors.forEach((anchor, i) => {
this.items[i].pos = this.findPos(anchor)['y'];
});
},
findPos(element) {
let posX = 0, posY = 0;
do {
posX += element.offsetLeft;
@@ -105,38 +105,38 @@ export default {
element = element.offsetParent;
}
while( element != null );
let pos = [];
pos['x'] = posX;
pos['y'] = posY;
return pos;
},
handleScroll(event) {
let pos = this.findPos(this.stickyNav);
let top = this.heightSum + this.top - window.scrollY;
//console.log(window.scrollY);
if (top > this.limit) {
//console.log(window.scrollY);
if (top > this.limit) {
this.stickyNav.style.position = 'absolute';
this.stickyNav.style.left = '10px';
} else {
this.stickyNav.style.position = 'fixed';
this.stickyNav.style.left = pos['x'] + 'px';
}
this.switchActive();
},
switchActive() {
this.items.forEach((item, i) => {
let next = (this.items[i+1]) ? this.items[i+1].pos : '100000';
item.active =
item.active =
(window.scrollY >= item.pos & window.scrollY < next) ? true : false;
}, this);
// last item never switch active because scroll reach bottom of page
// last item never switch active because scroll reach bottom of page
if (document.body.scrollHeight == window.scrollY + window.innerHeight) {
this.items[this.items.length-1].active = true;
this.items[this.items.length-2].active = false;
@@ -151,7 +151,7 @@ export default {
<style lang="scss">
div#content {
position: relative;
div#navmap {
position: absolute;
top: 30px;
@@ -170,7 +170,7 @@ div#content {
span {
display: none;
}
&:hover,
&:hover,
&.active {
span {
display: inline;

View File

@@ -1,7 +1,7 @@
<template>
<a
v-if="item.key <= 7"
:href="item.name"
v-if="item.key <= 8"
:href="item.id"
:class="{ 'active': isActive }"
>
<i class="fa fa-fw fa-square"></i>
@@ -9,7 +9,7 @@
</a>
<a
v-else-if="step === 'DRAFT'"
:href="item.name"
:href="item.id"
:class="{ 'active': isActive }"
>
<i class="fa fa-fw fa-square"></i>

View File

@@ -42,6 +42,14 @@ const appMessages = {
enddate: "Date de sortie",
add_persons: "Ajouter des usagers",
date_start_to_end: "Participation du {start} au {end}",
leave_course: "L'usager quitte le parcours",
sure: "Êtes-vous sûr ?",
sure_description: "Une fois confirmé, il ne sera pas possible de faire marche arrière ! La sortie reste cependant consignée dans l'historique du parcours.",
ok: "Oui, l'usager quitte le parcours",
show_household_number: "Voir le ménage (n° {id})",
show_household: "Voir le ménage",
person_without_household_warning: "Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur appartenance à un ménage dès que possible.",
update_household: "Modifier l'appartenance",
},
requestor: {
title: "Demandeur",
@@ -78,6 +86,10 @@ const appMessages = {
person_locator: "Parcours localisé auprès de {0}",
no_address: "Il n'y a pas d'adresse associée au parcours"
},
scopes: {
title: "Services",
add_at_least_one: "Indiquez au moins un service",
},
referrer: {
title: "Référent du parcours",
label: "Vous pouvez choisir un TMS ou vous assigner directement comme référent",
@@ -105,6 +117,7 @@ const appMessages = {
participation_not_valid: "sélectionnez au minimum 1 usager",
socialIssue_not_valid: "sélectionnez au minimum une problématique sociale",
location_not_valid: "indiquez au minimum une localisation temporaire du parcours",
set_a_scope: "indiquez au moins un service",
sure: "Êtes-vous sûr ?",
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",
ok: "Confirmer le parcours"

View File

@@ -1,28 +1,41 @@
import 'es6-promise/auto';
import { createStore } from 'vuex';
import { fetchScopes } from 'ChillMainAssets/lib/api/scope.js';
import { getAccompanyingCourse,
patchAccompanyingCourse,
confirmAccompanyingCourse,
postParticipation,
postRequestor,
postResource,
postSocialIssue } from '../api';
postSocialIssue,
addScope,
removeScope,
} from '../api';
const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId;
let initPromise = getAccompanyingCourse(id)
.then(accompanying_course => new Promise((resolve, reject) => {
let scopesPromise = fetchScopes();
let accompanyingCoursePromise = getAccompanyingCourse(id);
let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
.then(([scopes, accompanyingCourse]) => new Promise((resolve, reject) => {
const store = createStore({
strict: debug,
modules: {
},
state: {
accompanyingCourse: accompanying_course,
accompanyingCourse: accompanyingCourse,
addressContext: {},
errorMsg: []
errorMsg: [],
// all the available scopes
scopes: scopes,
// the scopes at start. If the user remove all scopes, we re-add those scopes, by security
scopesAtStart: accompanyingCourse.scopes.map(scope => scope),
// the scope states at server side
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
},
getters: {
isParticipationValid(state) {
@@ -34,11 +47,16 @@ let initPromise = getAccompanyingCourse(id)
isLocationValid(state) {
return state.accompanyingCourse.location !== null;
},
isScopeValid(state) {
console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
return state.accompanyingCourse.scopes.length > 0;
},
validationKeys(state, getters) {
let keys = [];
if (!getters.isParticipationValid) { keys.push('participation'); }
if (!getters.isLocationValid) { keys.push('location'); }
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
if (!getters.isScopeValid) { keys.push('scopes'); }
//console.log('getter keys', keys);
return keys;
},
@@ -129,14 +147,34 @@ let initPromise = getAccompanyingCourse(id)
state.addressContext = context;
},
updateLocation(state, r) {
//console.log('### mutation: set location attributes', r);
state.accompanyingCourse.location = r.location;
//console.log('### mutation: set location attributes', r);
state.accompanyingCourse.locationStatus = r.locationStatus;
if (r.locationStatus !== 'person') {
state.addressContext.addressId = r.location.address_id;
//console.log('mutation: update context addressId', state.addressContext.addressId);
}
state.accompanyingCourse.location = r.location;
state.accompanyingCourse.personLocation = r.personLocation;
},
setEditContextTrue(state) {
//console.log('### mutation: set edit context = true');
setEditContextTrue(state, payload) {
//console.log('### mutation: set edit context true with addressId', payload.addressId);
state.addressContext.edit = true;
state.addressContext.addressId = payload.addressId;
},
setScopes(state, scopes) {
state.accompanyingCourse.scopes = scopes;
},
addScopeAtBackend(state, scope) {
let scopeIds = state.scopesAtBackend.map(s => s.id);
if (!scopeIds.includes(scope.id)) {
state.scopesAtBackend.push(scope);
}
},
removeScopeAtBackend(state, scope){
let scopeIds = state.scopesAtBackend.map(s => s.id);
if (scopeIds.includes(scope.id)) {
state.scopesAtBackend = state.scopesAtBackend.filter(s => s.id !== scope.id);
}
}
},
actions: {
@@ -223,6 +261,107 @@ let initPromise = getAccompanyingCourse(id)
resolve();
})).catch((error) => { commit('catchError', error) });
},
/**
* Handle the checked/unchecked scopes
*
* When the user set the scopes in a invalid situation (when no scopes are cheched), this
* method will internally re-add the scopes as they were originally when the page was loaded, but
* this does not appears for the user (they remains unchecked). When the user re-add a scope, the
* scope is back in a valid state, and the store synchronize with the new state (all the original scopes
* are removed if necessary, and the new checked scopes is backed).
*
* So, for instance:
*
* at load:
*
* [x] scope A (at backend: [x])
* [x] scope B (at backend: [x])
* [ ] scope C (at backend: [ ])
*
* The user uncheck scope A:
*
* [ ] scope A (at backend: [ ] as soon as the operation finish)
* [x] scope B (at backend: [x])
* [ ] scope C (at backend: [ ])
*
* The user uncheck scope B. The state is invalid (no scope checked), so we go back to initial state when
* the page loaded):
*
* [ ] scope A (at backend: [x] as soon as the operation finish)
* [ ] scope B (at backend: [x] as soon as the operation finish)
* [ ] scope C (at backend: [ ])
*
* The user check scope C. The scopes are back to valid state. So we go back to synchronization with UI and
* backend):
*
* [ ] scope A (at backend: [ ] as soon as the operation finish)
* [ ] scope B (at backend: [ ] as soon as the operation finish)
* [x] scope C (at backend: [x] as soon as the operation finish)
*
* **Warning** There is a problem if the user check/uncheck faster than the backend is synchronized.
*
* @param commit
* @param state
* @param dispatch
* @param scopes
* @returns Promise
*/
setScopes({ commit, state, dispatch }, scopes) {
let currentServerScopesIds = state.scopesAtBackend.map(scope => scope.id);
let checkedScopesIds = scopes.map(scope => scope.id);
let removedScopesIds = currentServerScopesIds.filter(id => !checkedScopesIds.includes(id));
let addedScopesIds = checkedScopesIds.filter(id => !currentServerScopesIds.includes(id));
let lengthAfterOperation = currentServerScopesIds.length + addedScopesIds.length
- removedScopesIds.length;
if (lengthAfterOperation > 0 || (lengthAfterOperation === 0 && state.scopesAtStart.length === 0) ) {
return dispatch('updateScopes', {
addedScopesIds, removedScopesIds
}).then(() => {
// warning: when the operation of dispatch are too slow, the user may check / uncheck before
// the end of the synchronisation with the server (done by dispatch operation). Then, it leads to
// check/uncheck in the UI. I do not know of to avoid it.
commit('setScopes', scopes);
return Promise.resolve();
});
} else {
return dispatch('setScopes', state.scopesAtStart).then(() => {
commit('setScopes', scopes);
return Promise.resolve();
});
}
},
/**
* Internal function for the store to effectively update scopes.
*
* Return a promise which resolves when all update operation are
* successful and finished.
*
* @param state
* @param commit
* @param addedScopesIds
* @param removedScopesIds
* @return Promise
*/
updateScopes({ state, commit }, { addedScopesIds, removedScopesIds }) {
let promises = [];
state.scopes.forEach(scope => {
if (addedScopesIds.includes(scope.id)) {
promises.push(addScope(state.accompanyingCourse.id, scope).then(() => {
commit('addScopeAtBackend', scope);
return Promise.resolve();
}));
}
if (removedScopesIds.includes(scope.id)) {
promises.push(removeScope(state.accompanyingCourse.id, scope).then(() => {
commit('removeScopeAtBackend', scope);
return Promise.resolve();
}));
}
});
return Promise.all(promises);
},
postFirstComment({ commit }, payload) {
//console.log('## action: postFirstComment: payload', payload);
patchAccompanyingCourse(id, { type: "accompanying_period", initialComment: payload })
@@ -260,8 +399,8 @@ let initPromise = getAccompanyingCourse(id)
})).catch((error) => { commit('catchError', error) });
},
updateLocation({ commit }, payload) {
//console.log('## action: updateLocation', payload.locationStatusTo);
let body = { 'type': payload.entity, 'id': payload.entityId };
//console.log('## action: updateLocation', payload.locationStatusTo);
let body = { 'type': payload.target, 'id': payload.targetId };
let location = {};
if (payload.locationStatusTo === 'person') { // patch for person address (don't remove addressLocation)
location = { 'personLocation': { 'type': 'person', 'id': payload.personId }};
@@ -273,7 +412,7 @@ let initPromise = getAccompanyingCourse(id)
location = { 'personLocation': null };
}
Object.assign(body, location);
patchAccompanyingCourse(payload.entityId, body)
patchAccompanyingCourse(payload.targetId, body)
.then(accompanyingCourse => new Promise((resolve, reject) => {
commit('updateLocation', {
location: accompanyingCourse.location,

View File

@@ -35,7 +35,7 @@
<ul>
<li v-for="p in personsReachables" :key="p.id">
<input type="checkbox" :value="p.id" v-model="personsPicked">
<person :person="p"></person>
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
</li>
</ul>
</div>
@@ -124,7 +124,7 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import VueMultiselect from 'vue-multiselect';
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
const i18n = {
messages: {
@@ -145,7 +145,7 @@ export default {
name: 'App',
components: {
VueMultiselect,
Person,
PersonRenderBox,
},
methods: {
submit() {

View File

@@ -122,7 +122,7 @@
<ul>
<li v-for="p in personsReachables" :key="p.id">
<input v-model="personsPicked" :value="p.id" type="checkbox">
<person :person="p"></person>
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
</li>
</ul>
</div>
@@ -150,7 +150,7 @@
</div>
<div v-else>
<p>{{ handlingThirdParty.text }}</p>
<show-address :address="handlingThirdParty.address"></show-address>
<address-render-box :address="handlingThirdParty.address"></address-render-box>
<ul class="record_actions">
<li>
@@ -173,7 +173,7 @@
<ul>
<li v-for="t in thirdParties">
<p>{{ t.text }}</p>
<show-address :address="t.address"></show-address>
<address-render-box :address="t.address"></address-render-box>
<ul class="record_actions">
<button :title="$t('remove_thirdparty')" class="btn btn-remove"
@@ -229,9 +229,9 @@ import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
import AddResult from './components/AddResult.vue';
import AddEvaluation from './components/AddEvaluation.vue';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import ShowAddress from 'ChillMainAssets/vuejs/Address/components/ShowAddress.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
const i18n = {
messages: {
@@ -274,8 +274,8 @@ export default {
AddResult,
AddEvaluation,
AddPersons,
Person,
ShowAddress,
PersonRenderBox,
AddressRenderBox,
},
i18n,
data() {

View File

@@ -1,21 +0,0 @@
import { addressMessages } from 'ChillMainAssets/vuejs/Address/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: 'Enregistrer',
validFrom: 'Date du déménagement',
move_date: 'Date du déménagement',
back_to_the_list: 'Retour à la liste',
household_address_move_success: 'La nouvelle adresse du ménage est enregistrée',
household_address_edit_success: 'L\'adresse du ménage a été mise à jour',
loading: 'chargement en cours...'
}
};
Object.assign(appMessages.fr, addressMessages.fr);
export {
appMessages
};

View File

@@ -1,5 +1,5 @@
<template>
<h2>{{ $t('household_members_editor.concerned.title') }}</h2>
<h2 class="mt-4">{{ $t('household_members_editor.concerned.title') }}</h2>
<h3 v-if="needsPositionning">
{{ $t('household_members_editor.concerned.persons_to_positionnate') }}
@@ -25,7 +25,7 @@
<div class="item-row">
<div class="item-col">
<div>
<person :person="conc.person"></person>
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box>
</div>
<div v-if="conc.person.birthdate !== null">
{{ $t('person.born', {'gender': conc.person.gender} ) }}
@@ -126,7 +126,7 @@ div.person {
<script>
import { mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import MemberDetails from './MemberDetails.vue';
import { ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
@@ -135,7 +135,7 @@ export default {
components: {
AddPersons,
MemberDetails,
Person,
PersonRenderBox,
},
computed: {
...mapGetters([

View File

@@ -1,24 +1,71 @@
<template>
<h2>{{ $t('household_members_editor.household_part') }}</h2>
<h2 class="mt-4">{{ $t('household_members_editor.household_part') }}</h2>
<div v-if="hasHousehold">
<div>
<household-viewer :household="household"></household-viewer>
<div class="flex-table">
<div class="item-bloc">
<household-render-box :household="household" :isAddressMultiline="true"></household-render-box>
</div>
</div>
<div v-if="isHouseholdNew && !hasHouseholdAddress">
<h3 >À quelle adresse habite ce ménage ?</h3>
<div v-if="filterAddressesSuggestion.length > 0" class="flex-table householdAddressSuggestionList">
<div v-for="a in filterAddressesSuggestion" class="item-bloc">
<show-address :address="a"></show-address>
<button class="btn btn-action" @click="setHouseholdAddress(a)">
Le ménage habite cette adresse
</button>
</div>
</div>
<div v-else>
<span class="chill-no-data-statement">Aucune adresse à suggérer</span>
<div v-if="hasAddressSuggestion" class="householdAddressSuggestion my-5">
<h4 class="mb-3">
{{ $t('household_members_editor.household.where_live_the_household') }}
</h4>
<div class="accordion" id="addressSuggestions">
<div class="accordion-item">
<h2 class="accordion-header" id="heading_address_suggestions">
<button v-if="!showAddressSuggestion"
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
aria-expanded="false"
@click="toggleAddressSuggestion">
{{ $tc('household_members_editor.show_household_suggestion', countAddressSuggestion) }}
</button>
<button v-if="showAddressSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleAddressSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</h2>
<div class="accordion-collapse" id="collapse_address_suggestions"
aria-labelledby="heading_address_suggestions" data-bs-parent="#addressSuggestions">
<div v-if="showAddressSuggestion">
<div class="flex-table householdAddressSuggestionList">
<div v-for="a in filterAddressesSuggestion" class="item-bloc">
<div class="float-button bottom">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="setHouseholdAddress(a)">
{{ $t('household_members_editor.household.household_live_to_this_address') }}
</button>
</li>
</ul>
</div>
<ul class="list-content fa-ul">
<li>
<i class="fa fa-li fa-map-marker"></i>
<address-render-box :address="a"></address-render-box>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<ul class="record_actions">
@@ -28,49 +75,30 @@
:key="addAddress.key"
:options="addAddress.options"
:result="addAddress.result"
@submitAddress="setHouseholdCreatedAddress"
@addressChangedCallback="setHouseholdCreatedAddress"
ref="addAddress">
</add-address>
</li>
</ul>
</div>
<div v-if="isHouseholdNew && hasHouseholdAddress">
<ul class="record_actions">
<li >
<button class="btn btn-misc" @click="removeHouseholdAddress">
Supprimer cette adresse
{{ $t('household_members_editor.household.delete_this_address') }}
</button>
</li>
</ul>
</div>
</div>
<div v-else-if="isForceLeaveWithoutHousehold">
{{ $t('household_members_editor.household.will_leave_any_household') }}
</div>
<div v-else>
<div class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
</div>
<div v-else class="alert alert-info">{{ $t('household_members_editor.household.no_household_choose_one') }}</div>
<ul v-if="allowChangeHousehold" class="record_actions">
<li v-if="!showHouseholdSuggestion">
<button
class="btn btn-misc"
@click="toggleHouseholdSuggestion"
>
{{ $tc('household_members_editor.show_household_suggestion',
countHouseholdSuggestion) }}
</button>
</li>
<li v-if="showHouseholdSuggestion && hasHouseholdSuggestion">
<button
class="btn btn-misc"
@click="toggleHouseholdSuggestion"
>
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
</li>
<li v-if="allowHouseholdCreate">
<button class="btn btn-create" @click="createHousehold">
{{ $t('household_members_editor.household.create_household') }}
@@ -93,85 +121,73 @@
</li>
</ul>
<div class="householdSuggestions">
<div v-if="showHouseholdSuggestion && hasHouseholdSuggestion">
<p>{{ $t('household_members_editor.household_for_participants_accompanying_period') }}:</p>
<div class="householdSuggestionList">
<div
v-for="h in filterHouseholdSuggestionByAccompanyingPeriod"
class="item"
>
<household-viewer :household="h"></household-viewer>
<ul class="record_actions">
<li>
<button class="btn btn-misc" @click="selectHousehold(h)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div >
<div v-if="hasHouseholdSuggestion" class="householdSuggestions my-5">
<h4 class="mb-3">
{{ $t('household_members_editor.household_for_participants_accompanying_period') }} :
</h4>
<div class="accordion" id="householdSuggestions">
<div class="accordion-item">
<h2 class="accordion-header" id="heading_household_suggestions">
<button v-if="!showHouseholdSuggestion"
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
aria-expanded="false"
@click="toggleHouseholdSuggestion">
{{ $tc('household_members_editor.show_household_suggestion', countHouseholdSuggestion) }}
</button>
<button v-if="showHouseholdSuggestion"
class="accordion-button"
type="button"
data-bs-toggle="collapse"
aria-expanded="true"
@click="toggleHouseholdSuggestion">
{{ $t('household_members_editor.hide_household_suggestion') }}
</button>
<!-- disabled bootstrap behaviour: data-bs-target="#collapse_household_suggestions" aria-controls="collapse_household_suggestions" -->
</h2>
<div class="accordion-collapse" id="collapse_household_suggestions"
aria-labelledby="heading_household_suggestions" data-bs-parent="#householdSuggestions">
<div v-if="showHouseholdSuggestion">
<div class="flex-table householdSuggestionList">
<div v-for="h in filterHouseholdSuggestionByAccompanyingPeriod" class="item-bloc">
<household-render-box :household="h"></household-render-box>
<ul class="record_actions">
<li>
<button class="btn btn-sm btn-choose" @click="selectHousehold(h)">
{{ $t('household_members_editor.select_household') }}
</button>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss">
div.householdAddressSuggestionList {
/*
display: flex;
list-style-type: none;
padding: 0;
*/
& > li {
}
}
.householdSuggestionList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
& > .item {
margin-bottom: 0.8rem;
width: calc(50% - 1rem);
border: 1px solid var(--chill-light-gray);
padding: 0.5rem 0.5rem 0 0.5rem;
ul.record_actions {
margin-bottom: 0;
}
}
}
</style>
<script>
import { mapGetters, mapState } from 'vuex';
import HouseholdViewer from 'ChillPersonAssets/vuejs/_components/Household/Household.vue';
import ShowAddress from 'ChillMainAssets/vuejs/Address/components/ShowAddress.vue';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import AddAddress from 'ChillMainAssets/vuejs/Address/components/AddAddress.vue';
export default {
name: 'Household',
components: {
HouseholdViewer,
ShowAddress,
HouseholdRenderBox,
AddressRenderBox,
AddAddress,
},
data() {
return {
addAddress: {
context: {
entity: {
type: 'household_create',
target: {
name: 'household_create',
id: 0
},
edit: false,
@@ -179,18 +195,17 @@ export default {
},
key: 'household_new',
options: {
hideDateFrom: true,
bindModal: {
useDate: {
validFrom: true
},
button: {
text: {
create: null,
create: 'household_members_editor.household.or_create_new_address',
edit: null,
}
},
title: {
create: null,
create: 'household_members_editor.household.create_new_address',
edit: null,
},
}
@@ -204,11 +219,14 @@ export default {
'hasHouseholdSuggestion',
'countHouseholdSuggestion',
'filterHouseholdSuggestionByAccompanyingPeriod',
'hasAddressSuggestion',
'countAddressSuggestion',
'filterAddressesSuggestion',
'hasHouseholdAddress',
]),
...mapState([
'showHouseholdSuggestion',
'showAddressSuggestion'
]),
household() {
return this.$store.state.household;
@@ -249,8 +267,12 @@ export default {
toggleHouseholdSuggestion() {
this.$store.commit('toggleHouseholdSuggestion');
},
toggleAddressSuggestion() {
this.$store.commit('toggleAddressSuggestion');
},
selectHousehold(h) {
this.$store.dispatch('selectHousehold', h);
this.toggleHouseholdSuggestion();
},
removeHousehold() {
this.$store.dispatch('removeHousehold');
@@ -260,15 +282,56 @@ export default {
console.log('setHouseholdAddress', a);
this.$store.commit('setHouseholdAddress', a);
},
setHouseholdCreatedAddress() {
let payload = this.$refs.addAddress.submitNewAddress();
setHouseholdCreatedAddress(payload) {
console.log('setHouseholdAddress', payload);
this.$store.dispatch('setHouseholdNewAddress', payload);
},
removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress');
}
},
},
};
</script>
<style lang="scss">
div#household_members_editor div,
div.householdSuggestionList {
&.flex-table {
margin: 0;
div.item-bloc div.item-row div.item-col {
&:first-child {
width: 25%;
}
&:last-child {
display: initial;
}
}
}
}
/*
div.householdAddressSuggestionList {
display: flex;
list-style-type: none;
padding: 0;
& > li {}
}
.householdSuggestionList {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
& > .item {
margin-bottom: 0.8rem;
width: calc(50% - 1rem);
border: 1px solid var(--chill-light-gray);
padding: 0.5rem 0.5rem 0 0.5rem;
ul.record_actions {
margin-bottom: 0;
}
}
}
*/
</style>

View File

@@ -3,7 +3,7 @@
<div class="item-row">
<div class="item-col">
<div>
<person :person="conc.person"></person>
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box>
<span v-if="isHolder" class="badge bg-primary holder">
{{ $t('household_members_editor.holder') }}
</span>
@@ -73,14 +73,14 @@ div.participation-details {
<script>
import { mapGetters } from 'vuex';
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
export default {
name: 'MemberDetails',
components: {
Person,
PersonRenderBox,
ckeditor: CKEditor.component,
},
props: [

View File

@@ -7,10 +7,16 @@ const appMessages = {
household: {
no_household_choose_one: "Aucun ménage de destination. Choisissez un ménage. Les usagers concernés par la modification apparaitront ensuite.",
new_household: "Nouveau ménage",
create_household: "Créer un ménage",
create_household: "Créer un nouveau ménage de destination",
search_household: "Chercher un ménage",
will_leave_any_household: "Ne rejoignent pas de ménage",
leave_without_household: "Sans nouveau ménage"
leave_without_household: "Sans nouveau ménage",
where_live_the_household: "À quelle adresse habite ce ménage ?",
household_live_to_this_address: "Sélectionner l'adresse",
no_suggestions: "Aucune adresse à suggérer",
delete_this_address: "Supprimer cette adresse",
create_new_address: "Créer une nouvelle adresse",
or_create_new_address: "Ou créer une nouvelle adresse",
},
concerned: {
title: "Nouveaux membres du ménage",
@@ -29,10 +35,11 @@ const appMessages = {
remove_position: "Retirer des {position}",
remove_concerned: "Ne plus transférer",
household_part: "Ménage de destination",
suggestions: "Suggestions",
hide_household_suggestion: "Masquer les suggestions",
show_household_suggestion: 'Aucune suggestion | Afficher une suggestion | Afficher {count} suggestions',
household_for_participants_accompanying_period: "Ces ménages partagent le même parcours",
select_household: "Choisir ce ménage",
household_for_participants_accompanying_period: "Des ménages partagent le même parcours",
select_household: "Sélectionner le ménage",
dates_title: "Période de validité",
dates: {
start_date: "Début de validité",

View File

@@ -36,6 +36,7 @@ const store = createStore({
householdSuggestionByAccompanyingPeriod: [],
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
addressesSuggestion: [],
showAddressSuggestion: true,
warnings: [],
errors: []
},
@@ -73,6 +74,12 @@ const store = createStore({
.filter(h => h.id !== state.household.id)
;
},
hasAddressSuggestion(state, getters) {
return getters.filterAddressesSuggestion.length > 0;
},
countAddressSuggestion(state, getters) {
return getters.filterAddressesSuggestion.length;
},
filterAddressesSuggestion(state) {
if (state.household === null) {
return state.addressesSuggestion;
@@ -260,6 +267,9 @@ const store = createStore({
toggleHouseholdSuggestion(state) {
state.showHouseholdSuggestion = !state.showHouseholdSuggestion;
},
toggleAddressSuggestion(state) {
state.showAddressSuggestion = !state.showAddressSuggestion;
},
setWarnings(state, warnings) {
state.warnings = warnings;
// reset errors, which should come from servers
@@ -327,7 +337,7 @@ const store = createStore({
commit('forceLeaveWithoutHousehold');
dispatch('computeWarnings');
},
selectHousehold({ commit }, h) {
selectHousehold({ commit, dispatch }, h) {
commit('selectHousehold', h);
dispatch('computeWarnings');
},

View File

@@ -1,48 +0,0 @@
// CURRENTLY NOT IN USE
<template>
<li v-if="address" class="chill-entity entity-address">
<i v-if="options.with_picto == true" class="fa fa-fw fa-map-marker"></i>
<span v-if="options.render == 'list' || options.render == 'list'" :class="'address ' + {'multiline' : options.multiline === true}">
<!-- if address.street is not empty -->
<p v-if="address.street" class="street">{{ address.street }}
<!-- if address.streetNumber is not empty -->
<span v-if="address.streetNumber" class="streetnumber">{{ address.streetNumber }}</span>
</p>
<!-- if options['extended_infos'] -->
<div v-if="options.extended_infos == true">
<span v-if="address.floor" class="floor">{{ address.floor }}</span>
<span v-if="address.corridor" class="corridor">{{ address.corridor }}</span>
<span v-if="address.steps" class="steps">{{ address.steps }}</span>
<span v-if="address.buildingName" class="buildingName">{{ address.buildingName }}</span>
<span v-if="address.flat" class="flat">{{ address.flat }}</span>
<span v-if="address.distribution" class="distribution">{{ address.distribution }}</span>
<span v-if="address.extra" class="extra">{{ address.extra }}</span>
</div>
<!-- if address.postCode is not empty -->
<div v-if="address.postCode">
<p class="postcode">
<span class="code">{{ address.postCode.code }}</span>
<span class="name">{{ address.postCode.name }}</span>
</p>
<p class="country">{{ address.postCode.country.name }}</p>
</div>
</span>
</li>
</template>
<script>
export default{
name: "AddressRenderBox",
props: ['address', 'options']
}
</script>

View File

@@ -0,0 +1,126 @@
<template>
<section class="chill-entity entity-household">
<div class="item-row">
<div class="item-col">
<!-- identifier -->
<div v-if="isHouseholdNew()" class="h4">
<i class="fa fa-home"></i>
{{ $t('new_household') }}
</div>
<div v-else class="h4">
<i class="fa fa-home"></i>
{{ $t('household_number', { number: household.id } ) }}
</div>
</div>
<div class="item-col">
<ul class="list-content">
<!-- member part -->
<li v-if="hasCurrentMembers" class="members" :title="$t('current_members')">
<template v-for="m in currentMembers()" :key="m.id">
<person-render-box render="badge"
:person="m.person"
:options="{
isHolder: m.holder,
addLink: true
}">
</person-render-box>
</template>
</li>
<li v-else class="members" :title="$t('current_members')">
<p class="chill-no-data-statement">{{ $t('no_members_yet') }}</p>
</li>
<!-- address part -->
<li v-if="hasAddress()">
<address-render-box :address="household.current_address" :isMultiline="isMultiline"></address-render-box>
</li>
<li v-else>
<span class="chill-no-data-statement">{{ $t('no_current_address') }}</span>
</li>
</ul>
</div>
</div>
</section>
</template>
<script>
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
const i18n = {
"messages": {
"fr": {
"household_number": "Ménage n°{number}",
"current_members": "Membres actuels",
"no_current_address": "Sans adresse actuellement",
"new_household": "Nouveau ménage",
"no_members_yet": "Aucun membre actuellement",
"holder": "titulaire",
}
}
};
export default {
name: 'HouseholdRenderBox',
props: ['household', 'isAddressMultiline'],
components: {
PersonRenderBox,
AddressRenderBox,
},
i18n,
computed: {
isMultiline() {
return (typeof this.isAddressMultiline !== 'undefined') ? this.isAddressMultiline : false;
}
},
methods: {
hasCurrentMembers() {
return this.household.current_members_id.length > 0;
},
currentMembers() {
return this.household.members.filter(m => this.household.current_members_id.includes(m.id))
.sort((a, b) => {
if (a.position.ordering < b.position.ordering) {
return -1;
}
if (a.position.ordering > b.position.ordering) {
return 1;
}
if (a.holder && !b.holder) {
return -1;
}
if (!a.holder && b.holder) {
return 1;
}
return 0;
});
},
currentMembersLength() {
return this.household.current_members_id.length;
},
isHouseholdNew() {
return !Number.isInteger(this.household.id);
},
hasAddress() {
return this.household.current_address !== null;
}
}
};
</script>
<style lang="scss">
section.chill-entity {
&.entity-household {
ul.list-content li::marker {
content: '';
}
}
}
</style>

View File

@@ -1,14 +1,14 @@
<template>
<div class="item-bloc">
<div v-if="render === 'bloc'" class="item-bloc">
<section class="chill-entity entity-person">
<div class="item-row entity-bloc">
<div class="item-row entity-bloc">
<div class="item-col">
<div class="entity-label">
<div :class="'denomination h' + options.hLevel">
<a v-if="this.options.addLink == true" href="#">
<a v-if="options.addLink === true" :href="getUrl">
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
@@ -27,80 +27,121 @@
</div>
<p v-if="this.options.addInfo == true" class="moreinfo">
<p v-if="options.addInfo == true" class="moreinfo">
<i :class="'fa fa-fw ' + getGenderIcon" title="{{ getGender }}"></i>
<time v-if="person.birthdate" datetime="{{ person.birthdate }}" title="{{ birthdate }}">
{{ $t(getGender) + ' ' + $d(birthdate, 'text') }}
<time v-if="person.birthdate && !person.deathdate" datetime="{{ person.birthdate }}" title="{{ birthdate }}">
{{ $t(getGenderTranslation) + ' ' + $d(birthdate, 'text') }}
</time>
<time v-else-if="person.deathdate" datetime="{{ person.deathdate }}" title="{{ person.deathdate }}">
<time v-else-if="person.birthdate && person.deathdate" datetime="{{ person.deathdate }}" title="{{ person.deathdate }}">
{{ birthdate }} - {{ deathdate }}
</time>
<!-- <span class="age">{{ person.age }}</span> -->
<time v-else-if="person.deathdate" datetime="{{ person.deathdate }}" title="{{ person.deathdate }}">
{{ $t('renderbox.deathdate') + ' ' + deathdate }}
</time>
<span v-if="options.addAge && person.birthdate" class="age">{{ getAge }} {{ $t('renderbox.years_old')}}</span>
</p>
</div>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
<div class="float-button bottom">
<div class="box">
<div class="action">
<slot name="record-actions"></slot>
</div>
<ul class="list-content fa-ul">
<li v-if="person.current_household_address">
<i class="fa fa-li fa-map-marker"></i>
<show-address :address="person.current_household_address" :isMultiline="isMultiline"></show-address>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.current_household_id">
<i class="fa fa-li fa-map-marker"></i>
<address-render-box v-if="person.current_household_address"
:address="person.current_household_address"
:isMultiline="isMultiline">
</address-render-box>
<p v-else class="chill-no-data-statement">
{{ $t('renderbox.household_without_address') }}
</p>
<a v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl"
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
<span class="badge rounded-pill bg-chill-beige">
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
</span>
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker"></i>
<p class="chill-no-data-statement">
{{ $t('renderbox.no_data') }}
</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + person.mobilenumber">{{ person.mobilenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.phonenumber">
<i class="fa fa-li fa-phone"></i>
<a :href="'tel: ' + person.phonenumber">{{ person.phonenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + person.mobilenumber">{{ person.mobilenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.phonenumber">
<i class="fa fa-li fa-phone"></i>
<a :href="'tel: ' + person.phonenumber">{{ person.phonenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.center && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person.center.name }}
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<slot name="custom-zone"></slot>
<li v-if="person.center && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person.center.name }}
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<slot name="custom-zone"></slot>
</ul>
<slot name="record-actions"></slot>
</ul>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<span v-if="render === 'badge'" class="chill-entity entity-person badge-person">
<a v-if="options.addLink === true" :href="getUrl">
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{{ person.text }}
</a>
<span v-else>
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{{ person.text }}
</span>
</span>
</template>
<script>
import {dateToISO} from 'ChillMainAssets/chill/js/date.js';
import ShowAddress from 'ChillMainAssets/vuejs/Address/components/ShowAddress.vue';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
export default {
name: "PersonRenderBox",
components: {
ShowAddress
AddressRenderBox
},
props: ['person', 'options'],
props: ['person', 'options', 'render', 'returnPath'],
computed: {
getGender: function() {
getGenderTranslation: function() {
return this.person.gender == 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
},
isMultiline: function() {
@@ -114,23 +155,60 @@ export default {
return this.person.gender == 'woman' ? 'fa-venus' : this.person.gender == 'man' ? 'fa-mars' : 'fa-neuter';
},
birthdate: function(){
var date = new Date(this.person.birthdate.datetime);
return dateToISO(date);
if(this.person.birthdate !== null){
const date = new Date(this.person.birthdate.datetime);
return dateToISO(date)
} else {
return "";
}
},
deathdate: function(){
var date = new Date(this.person.deathdate.datetime);
return dateToISO(date);
// TODO FIX edition conflict: if null or undefined ?
// if (typeof this.person.deathdate !== 'undefined') {
// var date = new Date(this.person.deathdate.datetime);
// return dateToISO(date);
//}
if(this.person.deathdate !== null){
const date = new Date(this.person.deathdate.datetime);
return date.toLocaleDateString("fr-FR");
} else {
return "";
}
},
altNameLabel: function(){
altNameLabel: function() {
for(let i = 0; i < this.person.altNames.length; i++){
return this.person.altNames[i].label
}
},
altNameKey: function(){
altNameKey: function() {
for(let i = 0; i < this.person.altNames.length; i++){
return this.person.altNames[i].key
}
},
getUrl: function() {
return `/fr/person/${this.person.id}/general`;
},
getAge: function() {
if(this.person.birthdate && !this.person.deathdate){
const birthday = new Date(this.person.birthdate.datetime)
const now = new Date()
return (now.getFullYear() - birthday.getFullYear())
} else if(this.person.birthdate && this.person.deathdate){
const birthday = new Date(this.person.birthdate.datetime)
const deathdate = new Date(this.person.deathdate.datetime)
return (deathdate.getFullYear() - birthday.getFullYear())
} else if(!this.person.birthdate && this.person.deathdate.datetime) {
// todo: change this
return "Age unknown"
} else {
// todo: change this
return "Age unknown"
}
},
getCurrentHouseholdUrl: function() {
let returnPath = this.returnPath ? `?returnPath=${this.returnPath}` : ``;
return `/fr/person/household/${this.person.current_household_id}/summary${returnPath}`
}
}
}
</script>
@@ -149,10 +227,17 @@ div.flex-table {
}
div.item-col:last-child {
justify-content: flex-start;
}
}
}
}
.age{
margin-left: 0.5em;
&:before { content: '('; }
&:after { content: ')'; }
}
</style>

View File

@@ -1,139 +0,0 @@
<template>
<div class="chill-entity chill-entity__household">
<!-- identifier -->
<div v-if="isHouseholdNew()" class="identifier">
<i class="fa fa-home"></i>
{{ $t('new_household') }}
</div>
<div v-else class="identifier">
<i class="fa fa-home"></i>
{{ $t('household_number', { number: household.id } ) }}
</div>
<!-- member part -->
<div v-if="hasCurrentMembers" class="members">
<span class="current-members">{{ $t('current_members') }}: </span>
<template v-for="(m, index) in currentMembers()" :key="m.id">
<person :person="m.person"></person>
<span v-if="m.holder">
&nbsp;<span class="badge bg-primary">{{ $t('holder') }}</span>
</span>
<span v-if="index != (currentMembersLength() - 1)">, </span>
</template>
</div>
<div v-else class="members">
<p class="chill-no-data-statement">{{ $t('no_members_yet') }}</p>
</div>
<!-- address part -->
<div v-if="hasAddress()" class="where">
<i class="fa fa-where"></i>
<show-address :address="household.current_address"></show-address>
</div>
<div v-else class="where">
<i class="fa fa-where"></i>
<p class="chill-no-data-statement">{{ $t('no_current_address') }}</p>
</div>
</div>
</template>
<style lang="scss">
.chill-entity__household {
display: grid;
grid-template-areas:
"identifier identifier where"
"who who where"
;
grid-template-columns:
auto auto 30%
;
.identifier {
grid-area: identifier;
font-size: 1.3em;
font-weight: 700;
color: var(--chill-blue);
}
.members {
grid-area: who;
.current-members {
font-weight: 700;
}
}
.where {
grid-area: where
}
}
</style>
<script>
import Person from 'ChillPersonAssets/vuejs/_components/Person/Person.vue';
import ShowAddress from 'ChillMainAssets/vuejs/Address/components/ShowAddress.vue';
const i18n = {
"messages":
{
"fr":
{
"household_number": "Ménage #{number}",
"current_members": "Membres actuels",
"no_current_address": "Sans adresse actuellement",
"new_household": "Nouveau ménage",
"no_members_yet": "Aucun membre actuellement",
"holder": "titulaire",
}
}
}
;
export default {
name: 'Household',
props: ['household'],
components: {
Person,
ShowAddress,
},
i18n,
methods: {
hasCurrentMembers() {
return this.household.current_members_id.length > 0;
},
currentMembers() {
return this.household.members.filter(m => this.household.current_members_id.includes(m.id))
.sort((a, b) => {
if (a.position.ordering < b.position.ordering) {
return -1;
}
if (a.position.ordering > b.position.ordering) {
return 1;
}
if (a.holder && !b.holder) {
return -1;
}
if (!a.holder && b.holder) {
return 1;
}
return 0;
});
},
currentMembersLength() {
return this.household.current_members_id.length;
},
isHouseholdNew() {
return !Number.isInteger(this.household.id);
},
hasAddress() {
return this.household.current_address !== null;
}
}
};
</script>

View File

@@ -1,12 +1,13 @@
<template>
<div v-if="action === 'show'">
<div class="flex-table">
<person-render-box
<person-render-box render="bloc"
:person="person"
:options="{
addInfo: true,
addEntity: false,
addAltNames: true,
addAge: true,
addId: true,
addLink: false,
hLevel: 3,

View File

@@ -1,16 +0,0 @@
<template>
<span class="chill-entity chill-entity__person">
<span class="chill-entity__person__text chill_denomination">
{{ person.text }}
</span>
</span>
</template>
<script>
export default {
name: 'Person',
props: ['person']
}
</script>

View File

@@ -1,7 +1,7 @@
{% extends '@ChillPerson/AccompanyingCourse/layout.html.twig' %}
{% block title %}
{{ 'Accompanying Course Details'|trans }}
{{ 'Accompanying Course History'|trans }}
{% endblock %}
{% block content %}
@@ -15,8 +15,8 @@
Il faudrait peut-être modifier son adresse comme ceci: `/fr/parcours/{id}/timeline`
</p>
{# start test flex-table
{# start test flex-table
<div class="flex-table">
{% for p in accompanyingCourse.participations %}
<div class="item-bloc">
@@ -52,16 +52,16 @@
<li><button type="button" class="btn btn-edit"></button></li>
</ul>
</div>
</div>
<div class="item-row">
Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.
Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.
</div>
<div class="item-row">
Rhoncus est pellentesque elit eu ultrices vitae auctor.
Rhoncus est pellentesque elit eu ultrices vitae auctor.
</div>
<div class="item-row">
Facilisis gravida neque convallis a cras semper auctor neque.
Facilisis gravida neque convallis a cras semper auctor neque.
</div>
</div>
{% endfor %}
@@ -70,5 +70,5 @@
{# end test flex-table #}
{# ==> insert accompanyingCourse vue component #}
<div id="accompanying-course"></div>
<div id="accompanying-course"></div>
{% endblock %}

View File

@@ -34,6 +34,9 @@
</div>
{% endif %}
<pre>WIP .. AccompanyingCourse Resume dashboard</pre>
{#
<h1>{{ 'Resume Accompanying Course'|trans }}</h1>
<div class="associated-persons mb-5">
@@ -50,11 +53,13 @@
{% endif %}
{% endfor %}
</div>
#}
{% if 'DRAFT' != accompanyingCourse.step %}
{% if withoutHousehold|length > 0 %}
{% include '@ChillPerson/AccompanyingCourse/_join_household.html.twig' with {} %}
{% endif %}
{% endif %}
{#
</div>
<div class="location mb-5">
@@ -75,12 +80,11 @@
</div>
</div>
{% endif %}
#}
{% if accompanyingCourse.locationStatus == 'address' or accompanyingCourse.locationStatus == 'none' %}
{% include '@ChillPerson/AccompanyingCourse/_warning_address.html.twig' with {} %}
{% endif %}
{#
</div>
<div class="requestor mb-5">
@@ -128,6 +132,8 @@
</div>
{% endif %}
</div>
#}
<div class="mt-5"></div>
<div class="social-actions mb-5">
<h2 class="mb-3">{{ 'Social actions'|trans }}</h2>

View File

@@ -1,72 +0,0 @@
{#
This Twig template include load vue_address component.
It push all variables from context in Address/App.vue.
OPTIONS
* mode string ['edit*'|'new'|'create']
* address_id integer
* backUrl twig route: path('route', {parameters})
* modalTitle twig translated chain
* buttonText twig translated chain
* buttonSize bootstrap class like 'btn-sm'
#}
<div id="address"></div>
<script type="text/javascript">
window.vueRootComponent = 'app';
{% if person is defined %}
window.entityType = 'person';
window.entityId = {{ person.id|e('js') }};
{% elseif household is defined %}
window.entityType = 'household';
window.entityId = {{ household.id|e('js') }};
{% endif %}
{% if 'edit' in app.request.get('_route') %}
window.addressId = {{ app.request.get('address_id')|e('js') }};
window.mode = 'edit';
{% elseif mode is defined and mode == 'edit' %}
window.addressId = {{ address_id|e('js') }};
window.mode = 'edit';
{% endif %}
{% if backUrl is not defined %}
{% if person is defined %}
window.backUrl = '{{ path('chill_person_address_list', { 'person_id': person.id })|e('js') }}';
{% elseif household is defined %}
window.backUrl = '{{ path('chill_person_household_addresses', { 'household_id': household.id })|e('js') }}';
{% endif %}
{% else %}
window.backUrl = '{{ backUrl|e('js') }}';
{% endif %}
{% if modalTitle is defined %}
window.modalTitle = '{{ modalTitle|trans|e('js') }}';
{% endif %}
{% if buttonText is defined %}
window.buttonText = '{{ buttonText|trans|e('js') }}';
{% endif %}
{% if buttonSize is defined %}
window.buttonSize = '{{ buttonSize|e('js') }}';
{% endif %}
{% if buttonDisplayText is defined and buttonDisplayText == false %}
window.buttonDisplayText = false;
{% endif %}
{% if binModalStep1 is defined and binModalStep1 == false %}
window.binModalStep1 = false;
{% endif %}
{% if binModalStep2 is defined and binModalStep2 == false %}
window.binModalStep2 = false;
{% endif %}
</script>
{{ encore_entry_script_tags('vue_address') }}
{{ encore_entry_link_tags('vue_address') }}

View File

@@ -27,10 +27,15 @@
<h1>{{ block('title') }}</h1>
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
binModalStep1: false,
binModalStep2: false,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'person', id: person.id },
backUrl: path('chill_person_address_list', { 'person_id': person.id }),
openPanesInModal: false,
stickyActions: true,
useValidFrom: true,
} %}
{#
#}
{% endblock %}

View File

@@ -29,7 +29,9 @@
<li style="margin: auto;">
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'person', id: person.id },
backUrl: path('chill_person_address_list', { 'person_id': person.id }),
mode: 'new',
buttonSize: 'btn-lg',
buttonText: 'Add an address',
@@ -64,28 +66,30 @@
{% if address.validTo is not empty and address.validTo < previousRowFrom %}
<div class="{{ 'row' ~ row ~ ' ' }}col-a action">
{# include vue_address component #}
<a href="" class="btn btn-sm btn-create">{{ 'Insert an address'|trans }}</a></div>
{# include vue_address component #}
{#
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
address_id: address.id,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'person', id: person.id },
backUrl: path('chill_person_address_list', { 'person_id': person.id }),
mode: 'edit',
binModalStep1: false,
binModalStep2: false,
addressId: address.id,
openPanesInModal: false,
} %}
#}
<div class="{{ 'row' ~ row ~ ' ' }}col-b"></div>
<div class="{{ 'row' ~ row ~ ' ' }}col-c action">
{# include vue_address component #}
<a href="" class="btn btn-sm btn-create">{{ 'Insert an address'|trans }}</a></div>
{# include vue_address component #}
{#
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
address_id: address.id,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'person', id: person.id },
backUrl: path('chill_person_address_list', { 'person_id': person.id }),
mode: 'edit',
binModalStep1: false,
binModalStep2: false,
addressId: address.id,
openPanesInModal: false,
} %}
#}
@@ -109,7 +113,6 @@
}) }}
<ul class="record_actions">
{# include vue_address component #}
<li><a href="{{ path('chill_person_address_edit', { 'person_id': person.id, 'address_id' : address.id } ) }}" class="btn btn-edit"></a></li>
</ul>
@@ -160,7 +163,6 @@
</a>
</li>
<li>
{# include vue_address component #}
<a class="btn btn-create"
href="{{ path('chill_person_address_new', { 'person_id' : person.id } ) }}">
{{ 'Add an address'|trans }}

View File

@@ -27,10 +27,16 @@
<h1>{{ block('title') }}</h1>
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
binModalStep1: false,
binModalStep2: false,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'person', id: person.id },
backUrl: path('chill_person_address_list', { 'person_id': person.id }),
openPanesInModal: false,
stickyActions: true,
useValidFrom: true,
} %}
{#
useValidTo: true,
#}
{% endblock %}

View File

@@ -16,6 +16,10 @@
'replace' Twig\Markup,
'after' Twig\Markup
]
* customArea [
'beforeLabel' Twig\Markup,
'afterLabel' Twig\Markup,
]
#}
{% macro raw(person, options) %}
@@ -37,11 +41,22 @@
<div class="denomination {{ 'h' ~ options['hLevel'] }}">
{%- if options['addLink'] and is_granted('CHILL_PERSON_SEE', person) -%}
<a href="{{ chill_path_add_return_path('chill_person_view', { 'person_id': person.id }) }}">
{{ _self.raw(person, options) }}
</a>
{%- else -%}
{{ _self.raw(person, options) }}
{%- endif -%}
{% if options['customArea']['beforeLabel'] is defined %}
{{ options['customArea']['beforeLabel'] }}
{% endif %}
{{ _self.raw(person, options) }}
{% if options['customArea']['afterLabel'] is defined %}
{{ options['customArea']['afterLabel'] }}
{% endif %}
{%- if options['addLink'] and is_granted('CHILL_PERSON_SEE', person) -%}
</a>
{%- endif -%}
{%- if options['addEntity'] -%}
<span class="badge rounded-pill bg-secondary">{{ 'Person'|trans }}</span>
{%- endif -%}
@@ -131,10 +146,10 @@
{% endif %}
{% endif %}
</li>
{% if options['addCenter'] %}
{% if options['addCenter'] and person|chill_resolve_center is not null %}
<li>
<i class="fa fa-li fa-long-arrow-right"></i>
{{ person.center }}
{{ person|chill_resolve_center.name }}
</li>
{% endif %}
</ul>

View File

@@ -4,7 +4,8 @@
{% block content %}
<h1>{{ block('title') }}</h1>
<h1>{{ member.person|chill_entity_render_string }}</h1>
<h2 class="mb-5">{{ 'household.Edit his household'|trans }}</h2>
{{ form_start(form) }}
{{ form_widget(form) }}

View File

@@ -0,0 +1,65 @@
{% macro addHolder(holder) %}
{% if holder %}
<span class="fa-stack fa-holder" title="{{ 'household.holder'|trans }}">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{% endif %}
{% endmacro %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col" style="flex-basis: 30%;">
{{ member.person|chill_entity_render_box({
'render': 'label',
'addLink': true,
'addInfo': true,
'customArea': {
'afterLabel': _self.addHolder(member.holder)
}
}) }}
</div>
<div class="item-col">
<div class="float-button top"><div class="box">
<div class="action">
<ul class="record_actions">
{% if customButtons['before'] is defined %}
{{ customButtons['before'] }}
{% endif %}
<li>
<a class="btn btn-sm btn-edit"
title="{{ 'household.Edit member household'|trans }}"
href="{{ chill_path_add_return_path('chill_person_household_member_edit', { 'id': member.id }) }}"></a>
</li>
{% if customButtons['after'] is defined %}
{{ customButtons['after'] }}
{% endif %}
</ul>
</div>
<ul class="list-content fa-ul small ms-0">
{% if member.startDate is not empty %}
<li><i class="fa fa-long-arrow-right"></i>
{{ 'Since %date%'|trans({'%date%': member.startDate|format_date('short') }) }}</li>
{% endif %}
{% if member.endDate is not empty %}
<li><i class="fa fa-long-arrow-right"></i>
{{ 'Until %date%'|trans({'%date%': member.endDate|format_date('short') }) }}</li>
{% endif %}
</ul>
{% if member.comment is not empty %}
<blockquote class="chill-user-quote ms-0 mb-0" style="clear: right">
{{ member.comment|chill_markdown_to_html }}
</blockquote>
{% endif %}
</div></div>
</div>
</div>
</div>

View File

@@ -7,9 +7,12 @@
<div>
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
binModalStep1: false,
binModalStep2: false,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'household', id: household.id },
backUrl: path('chill_person_household_addresses', { 'household_id': household.id }),
openPanesInModal: false,
stickyActions: true,
useValidFrom: true,
} %}
</div>

View File

@@ -7,10 +7,15 @@
<div>
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
binModalStep1: false,
binModalStep2: false,
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'household', id: household.id },
backUrl: path('chill_person_household_addresses', { 'household_id': household.id }),
openPanesInModal: false,
stickyActions: true,
useValidFrom: true,
} %}
{#
#}
</div>
{% endblock %}

View File

@@ -15,13 +15,17 @@
<li style="margin: auto;">
{# include vue_address component #}
{% include '@ChillPerson/Address/_insert_vue_address.html.twig' with {
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'household', id: household.id },
backUrl: path('chill_person_household_addresses', { 'household_id': household.id }),
mode: 'new',
backUrl: chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }),
buttonSize: 'btn-lg',
buttonText: 'Move household',
modalTitle: 'Move household',
} %}
{#
useValidFrom: true,
#}
</li>
</ul>
@@ -41,7 +45,6 @@
<div class="{{ 'row' ~ row ~ ' ' }}col-b"></div>
<div class="{{ 'row' ~ row ~ ' ' }}col-c action">
{# include vue_address component #}
<a href="" class="btn btn-sm btn-create">{{ 'Insert an address'|trans }}</a></div>
<div class="date">
@@ -63,8 +66,6 @@
}) }}
<ul class="record_actions">
<li>
{# include vue_address component #}
<a href="{{ path('chill_person_household_address_edit', { 'household_id': household.id, 'address_id' : address.id } ) }}"
class="btn btn-edit"></a>
@@ -91,7 +92,6 @@
</li>
<li>
{# include vue_address component #}
<a class="btn btn-create"
href="{{ chill_path_add_return_path('chill_person_household_address_move', { 'household_id': household.id }) }}">
{{ 'Move household'|trans }}

View File

@@ -20,24 +20,21 @@
<i class="fa fa-fw fa-users" title="{{- 'household.Current household members'|trans }}"></i>
</span>
{%- for m in members|slice(0, 5) -%}
{%- for m in members -%}
<span
class="badge-member{%- if m.holder %} holder{% endif -%}{%- if m.position.ordering >= 2 %} child{% endif -%}"
title="{{ m.position.label.fr }}">
{%- if m.holder %}
<span class="badge bg-chill-light-gray text-chill-gray">
{{ 'household.holder'|trans }}
</span>
{% endif -%}
{{- m.person|chill_entity_render_box({'addLink': false}) -}}
<a href="{{ path('chill_person_view', { person_id: m.person.id}) }}">
{%- if m.holder %}
<span class="fa-stack fa-holder" title="{{ 'household.holder'|trans }}">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{% endif -%}
{{- m.person|chill_entity_render_box() -}}
</a>
</span>
{%- endfor -%}
{% if members|length > 5 %}
<span class="current-members-more">
{{ 'household.and x other persons'|trans({'x': members|length-5}) }}
</span>
{% endif %}
{%- endif -%}
</div>
</div>

View File

@@ -17,4 +17,8 @@
'layout': '@ChillPerson/menu.html.twig',
'args' : { 'household': household }
}) }}
{% block block_post_menu %}
{% endblock %}
{% endblock %}

View File

@@ -4,7 +4,7 @@
{% block title 'household.Edit household members'|trans %}
{% block content %}
<div class="col-md-10 col-xxl household-members">
<div class="household-members">
<h1>{{ block('title') }}</h1>
<div id="household_members_editor"></div>
@@ -22,5 +22,6 @@
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('vue_household_members_editor') }}
{{ parent() }}
{{ encore_entry_link_tags('vue_household_members_editor') }}
{% endblock %}

View File

@@ -2,213 +2,196 @@
{% block title 'household.Household summary'|trans %}
{% block block_post_menu %}
<div class="block-post-menu"></div>
{% endblock %}
{% block content %}
<div class="household-summary">
<h1>{{ block('title') }}</h1>
{#
<h1>{{ block('title') }}</h1>
<h2>{{ 'household.Current address'|trans }}</h2>
#}
<h2>{{ 'household.Current address'|trans }}</h2>
{% set address = household.currentAddress %}
{% set address = household.currentAddress %}
<div class="row household-resume">
<div class="item-bloc col-5 col-address">
<h2>{{ 'Address'|trans }}</h2>
{% if address is empty %}
<p class="chill-no-data-statement">{{ 'household.Household does not have any address currently'|trans }}</p>
{% else %}
<div>
{{ address|chill_entity_render_box({'multiline': true}) }}
</div>
{% endif %}
<h2>{{ 'household.Household members'|trans }}</h2>
{% if form is not null %}
{{ form_start(form) }}
{{ form_row(form.commentMembers) }}
<div id="waitingForBirthContainer">
{{ form_row(form.waitingForBirth) }}
</div>
<div id="waitingForBirthDateContainer">
{{ form_row(form.waitingForBirthDate) }}
</div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save">
{{ 'Save'|trans }}
</button>
</li>
</ul>
{{ form_end(form) }}
{% else %}
{% if not household.commentMembers.isEmpty() %}
{{ household.commentMembers|chill_entity_render_box }}
{% endif %}
{% if household.waitingForBirth %}
{% if household.waitingForBirthDate is not null %}
{{ 'household.Expecting for birth on date'|trans({ 'date': household.waitingForBirthDate|format_date('long') }) }}
{% else %}
{{ 'household.Expecting for birth'|trans }}
{% endif %}
{% else %}
<p class="chill-no-data-statement">
{{ 'household.Any expecting birth'|trans }}
</p>
{% endif %}
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id': household.id, 'edit': 1 }) }}"
class="btn btn-edit">
{{ 'household.Comment and expecting birth'|trans }}
</a>
</li>
</ul>
{% endif %}
{% for p in positions %}
<h3>{{ p.label|localize_translatable_string }}</h3>
{% if false == p.shareHousehold %}
<p>{{ 'household.Those members does not share address'|trans }}</p>
{% endif %}
{%- set members = household.currentMembersByPosition(p) %}
{% if members|length > 0 %}
<div class="flex-table list-household-members">
{% for m in members %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<div>
{{ m.person|chill_entity_render_box({'addLink': true}) }}
{% if m.holder %}
<span class="badge bg-primary">{{ 'household.holder'|trans }}</span>
{% endif %}
</div>
<div>
{{ 'Born the date'|trans({ 'gender': m.person.gender, 'birthdate': m.person.birthdate|format_date('long') }) }}
</div>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
{% if m.startDate is not empty %}
<li>{{ 'Since %date%'|trans({'%date%': m.startDate|format_date('long') }) }}</li>
{% endif %}
{% if m.endDate is not empty %}
<li>{{ 'Until %date%'|trans({'%date%': m.endDate|format_date('long') }) }}</li>
{% endif %}
</ul>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_member_edit', { 'id': m.id }) }}"
class="btn btn-edit" />
{{ 'household.Update membership'|trans }}
</a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ m.person.id ], 'household': household.id} ) }}"
class="btn btn-misc" />
<i class="fa fa-arrows-h"></i>
{{ 'household.Change position'|trans }}
</a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ m.person.id ], 'allow_leave_without_household': true } ) }}"
class="btn btn-misc" />
<i class="fa fa-sign-out"></i>
{{ 'household.Leave'|trans }}
</a>
</li>
</ul>
</div>
</div>
{% if m.comment is not empty %}
<div class="item-row comment">
<blockquote class="chill-user-quote">
{{ m.comment|chill_markdown_to_html }}
</blockquote>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="chill-no-data-statement">{{ 'household.Any persons into this position'|trans }}</p>
{% endif %}
{% set members = household.nonCurrentMembersByPosition(p) %}
{% if members|length > 0 %}
<p><!-- force a space after table --></p>
<button class="btn btn-green" type="button" data-bs-toggle="collapse" data-bs-target="#nonCurrent_{{ p.id }}" aria-expanded="false" aria-controls="collapse non current members">
{{ 'household.Show future or past memberships'|trans({'length': members|length}) }}
</button>
<div id="nonCurrent_{{ p.id }}" class="collapse">
<div class="flex-table list-household-members">
{% for m in members %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
<div>
{{ m.person|chill_entity_render_box({'addLink': true}) }}
{% if m.holder %}
<span class="badge bg-primary">{{ 'household.holder'|trans }}</span>
{% endif %}
</div>
<div>
{{ 'Born the date'|trans({ 'gender': m.person.gender, 'birthdate': m.person.birthdate|format_date('long') }) }}
</div>
</div>
<div class="item-col">
<ul class="list-content fa-ul">
{% if m.startDate is not empty %}
<li>{{ 'Since %date%'|trans({'%date%': m.startDate|format_date('long') }) }}</li>
{% endif %}
{% if m.endDate is not empty %}
<li>{{ 'Until %date%'|trans({'%date%': m.endDate|format_date('long') }) }}</li>
{% endif %}
</ul>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_member_edit', { 'id': m.id }) }}"
class="btn btn-edit">
{{ 'household.Update membership'|trans }}
</a>
</li>
</ul>
</div>
</div>
{% if m.comment is not empty %}
<div class="item-row comment">
<blockquote class="chill-user-quote">
{{ m.comment|chill_markdown_to_html }}
</blockquote>
</div>
{% if address is empty %}
<p class="chill-no-data-statement">{{ 'household.Household does not have any address currently'|trans }}</p>
{% else %}
{{ address|chill_entity_render_box({'multiline': true, 'extended_infos': true }) }}
{% endif %}
</div>
{% endfor %}
</div>
<ul class="list-inline text-right mt-2">
<li class="list-inline-item">
{# include vue_address component #}
{% include '@ChillMain/Address/_insert_vue_address.html.twig' with {
targetEntity: { name: 'household', id: household.id },
backUrl: path('chill_person_household_summary', { 'household_id': household.id }),
hideAddress: true,
mode: 'new',
buttonSize: 'btn-sm',
buttonText: 'Move household',
modalTitle: 'Move household',
buttonDisplayText: false,
} %}
</li>
<li class="list-inline-item">
<a class="btn btn-secondary btn-sm" title="{{ "Addresses history"|trans }}"
href="{{ path('chill_person_household_addresses', { 'household_id': household.id } ) }}">
<i class="fa fa-list fa-fw"></i>
</a>
</li>
</ul>
</div>
{% if address is not empty %}
<div class="item-bloc col-7 col-comment">
{% if form is null %}
{% if household.waitingForBirth or not household.commentMembers.isEmpty() %}
<div class="p-4 bg-light">
{% if household.waitingForBirth %}
<i class="fa fa-check-square-o pe-2"></i>
{% if household.waitingForBirthDate is not null %}
{{ 'household.Expecting for birth on date'|trans({ 'date': household.waitingForBirthDate|format_date('long') }) }}
{% else %}
{{ 'household.Expecting for birth'|trans }}
{% endif %}
{% endif %}
{% if not household.commentMembers.isEmpty() %}
{{ household.commentMembers|chill_entity_render_box }}
{% endif %}
</div>
{% endif %}
{% if not household.commentMembers.isEmpty() %}
<a href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id': household.id, 'edit': 1 }) }}"
class="btn btn-edit btn-block">
{{ 'household.Edit comment and expecting birth'|trans }}
</a>
{% else %}
<a href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id': household.id, 'edit': 1 }) }}"
class="btn btn-create btn-block">
{{ 'household.New comment and expecting birth'|trans }}
</a>
{% endif %}
{% else %}
{{ form_start(form) }}
<div id="waitingForBirthContainer">
{{ form_widget(form.waitingForBirth) }}
</div>
<div id="waitingForBirthDateContainer">
{{ form_widget(form.waitingForBirthDate) }}
</div>
<div class="mt-3">
{{ form_widget(form.commentMembers) }}
</div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save">
{{ 'Save'|trans }}
</button>
</li>
</ul>
{{ form_end(form) }}
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
<h2 class="my-5">{{ 'household.Household members'|trans }}</h2>
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'household': household.id }) }}"
class="btn btn-create">
{{ 'household.Add a member'|trans }}
</a>
</li>
</ul>
{% for p in positions %}
<div class="mb-5">
<h3>{{ p.label|localize_translatable_string }}
{% if false == p.shareHousehold %}
<i class="chill-help-tooltip" data-bs-toggle="tooltip" data-bs-placement="top" data-bs-html="true"
title="{{ 'household.Those members does not share address'|trans }}"></i>
{% endif %}
</h3>
{%- set members = household.currentMembersByPosition(p) %}
{% macro customButtons(member, household) %}
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ member.person.id ], 'allow_leave_without_household': true } ) }}"
class="btn btn-sm btn-unlink" title="{{ 'household.person.leave'|trans }}"></a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'persons': [ member.person.id ], 'household': household.id} ) }}"
class="btn btn-sm btn-misc" title="{{ 'household.Change position'|trans }}"><i class="fa fa-arrows-h"></i></a>
</li>
{% endmacro %}
{% if members|length > 0 %}
<div class="flex-table list-household-members">
{% for m in members %}
{% include '@ChillPerson/Household/_render_member.html.twig' with {
'member': m,
'customButtons': { 'after': _self.customButtons(m, household) }
} %}
{% endfor %}
</div>
{% else %}
<p class="chill-no-data-statement">{{ 'household.Any persons into this position'|trans }}</p>
{% endif %}
{% set members = household.nonCurrentMembersByPosition(p) %}
{% if members|length > 0 %}
<style>
button[aria-expanded="true"] > span.folded,
button[aria-expanded="false"] > span.unfolded { display: none; }
button[aria-expanded="false"] > span.folded,
button[aria-expanded="true"] > span.unfolded { display: inline; }
</style>
<div class="accordion" id="nonCurrent">
<div class="accordion-item">
<h2 class="accordion-header" id="heading_{{ p.id }}">
<button
class="accordion-button collapsed"
type="button"
data-bs-toggle="collapse"
data-bs-target="#collapse_{{ p.id }}"
aria-expanded="false"
aria-controls="collapse_{{ p.id }}">
<span class="folded">{{ 'household.Show future or past memberships'|trans({'length': members|length}) }}</span>
<span class="unfolded text-secondary">{{ 'household.Hide memberships'|trans }}</span>
</button>
</h2>
<div id="collapse_{{ p.id }}"
class="accordion-collapse collapse"
aria-labelledby="heading_{{ p.id }}"
data-bs-parent="#nonCurrent">
<div class="flex-table my-0 list-household-members">
{% for m in members %}
{% include '@ChillPerson/Household/_render_member.html.twig' with { 'member': m } %}
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_members_editor', {'household': household.id }) }}"
class="btn btn-create">
{{ 'household.Add a member'|trans }}
</a>
</li>
</ul>
</div>
{% endblock %}

View File

@@ -17,11 +17,11 @@
{%- endif -%}
</div>
<div class="text-md-end">
{%- if chill_person.fields.spoken_languages == 'visible' -%}
{% if person|chill_resolve_center is not null%}
<span class="open_sansbold">
{{ 'Center'|trans|upper}} :
</span>
{{ person.center.name|upper }}
{{ person|chill_resolve_center.name|upper }}
{%- endif -%}
</div>
</div>

View File

@@ -64,10 +64,10 @@
{{ form_start(form) }}
{{ form_row(form.firstName, { 'label' : 'First name'|trans }) }}
{{ form_row(form.lastName, { 'label' : 'Last name'|trans }) }}
{{ form_row(form.firstName, { 'label' : 'First name'|trans }) }}
{% if form.altNames is defined %}
{{ form_widget(form.altNames) }}
{% endif %}
@@ -76,12 +76,27 @@
{{ form_row(form.gender, { 'label' : 'Gender'|trans }) }}
{% if form.center is defined %}
{{ form_row(form.center) }}
{% endif %}
<ul class="record_actions sticky-form-buttons">
<li>
{{ form_widget(form.editPerson, { 'attr': { 'class': 'btn btn-create' }}) }}
</li>
<li>
{{ form_widget(form.createPeriod, { 'attr': { 'class': 'btn btn-create' }}) }}
<li class="dropdown">
<a class="btn btn-create dropdown-toggle"
href="#" role="button" id="newPersonMore" data-bs-toggle="dropdown" aria-expanded="false">
{{ 'Add the person'|trans }}
</a>
<ul class="dropdown-menu" aria-labelledby="newPersonMore">
<li>
{{ form_widget(form.editPerson, { 'attr': { 'class': 'dropdown-item' }}) }}
</li>
<li>
{{ form_widget(form.createPeriod, { 'attr': { 'class': 'dropdown-item' }}) }}
</li>
<li>
{{ form_widget(form.createHousehold, { 'attr': { 'class': 'dropdown-item' }}) }}
</li>
</ul>
</li>
</ul>

View File

@@ -20,71 +20,39 @@
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Search\AbstractSearch;
use Doctrine\ORM\EntityManagerInterface;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Search\SearchInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Chill\MainBundle\Search\ParsingException;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Symfony\Component\Security\Core\Role\Role;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\MainBundle\Search\HasAdvancedSearchFormInterface;
use Doctrine\ORM\Query;
use Symfony\Component\Templating\EngineInterface;
class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
HasAdvancedSearchFormInterface
class PersonSearch extends AbstractSearch implements HasAdvancedSearchFormInterface
{
use ContainerAwareTrait;
/**
*
* @var EntityManagerInterface
*/
private $em;
/**
*
* @var \Chill\MainBundle\Entity\User
*/
private $user;
/**
*
* @var AuthorizationHelper
*/
private $helper;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
protected EngineInterface $templating;
protected PaginatorFactory $paginatorFactory;
protected PersonACLAwareRepositoryInterface $personACLAwareRepository;
const NAME = "person_regular";
private const POSSIBLE_KEYS = [
'_default', 'firstname', 'lastname', 'birthdate', 'birthdate-before',
'birthdate-after', 'gender', 'nationality'
];
public function __construct(
EntityManagerInterface $em,
TokenStorageInterface $tokenStorage,
AuthorizationHelper $helper,
PaginatorFactory $paginatorFactory)
{
$this->em = $em;
$this->user = $tokenStorage->getToken()->getUser();
$this->helper = $helper;
EngineInterface $templating,
PaginatorFactory $paginatorFactory,
PersonACLAwareRepositoryInterface $personACLAwareRepository
) {
$this->templating = $templating;
$this->paginatorFactory = $paginatorFactory;
// throw an error if user is not a valid user
if (!$this->user instanceof \Chill\MainBundle\Entity\User) {
throw new \LogicException('The user provided must be an instance'
. ' of Chill\MainBundle\Entity\User');
}
$this->personACLAwareRepository = $personACLAwareRepository;
}
/*
@@ -120,12 +88,14 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
$paginator = $this->paginatorFactory->create($total);
if ($format === 'html') {
return $this->container->get('templating')->render('@ChillPerson/Person/list_with_period.html.twig',
return $this->templating->render('@ChillPerson/Person/list_with_period.html.twig',
array(
'persons' => $this->search($terms, $start, $limit, $options),
'pattern' => $this->recomposePattern($terms, array('nationality',
'firstname', 'lastname', 'birthdate', 'gender',
'birthdate-before','birthdate-after'), $terms['_domain']),
'pattern' => $this->recomposePattern(
$terms,
\array_filter(self::POSSIBLE_KEYS, fn($item) => $item !== '_default'),
$terms['_domain']
),
'total' => $total,
'start' => $start,
'search_name' => self::NAME,
@@ -152,153 +122,81 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
*/
protected function search(array $terms, $start, $limit, array $options = array())
{
$qb = $this->createQuery($terms, 'search');
if ($options['simplify'] ?? false) {
$qb->select(
'p.id',
$qb->expr()->concat(
'p.firstName',
$qb->expr()->literal(' '),
'p.lastName'
).'AS text'
);
} else {
$qb->select('p');
[
'_default' => $default,
'firstname' => $firstname,
'lastname' => $lastname,
'birthdate' => $birthdate,
'birthdate-before' => $birthdateBefore,
'birthdate-after' => $birthdateAfter,
'gender' => $gender,
'nationality' => $countryCode,
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
if (NULL !== ${$v}) {
try {
${$v} = new \DateTime(${$v});
} catch (\Exception $e) {
throw new ParsingException('The date is '
. 'not parsable', 0, $e);
}
}
}
$qb
->setMaxResults($limit)
->setFirstResult($start);
//order by firstname, lastname
$qb
->orderBy('p.firstName')
->addOrderBy('p.lastName');
if ($options['simplify'] ?? false) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
return $this->personACLAwareRepository
->findBySearchCriteria(
$start,
$limit,
$options['simplify'] ?? false,
$default,
$firstname,
$lastname,
$birthdate,
$birthdateBefore,
$birthdateAfter,
$gender,
$countryCode,
);
}
protected function count(array $terms)
protected function count(array $terms): int
{
$qb = $this->createQuery($terms);
[
'_default' => $default,
'firstname' => $firstname,
'lastname' => $lastname,
'birthdate' => $birthdate,
'birthdate-before' => $birthdateBefore,
'birthdate-after' => $birthdateAfter,
'gender' => $gender,
'nationality' => $countryCode,
$qb->select('COUNT(p.id)');
] = $terms + \array_fill_keys(self::POSSIBLE_KEYS, null);
return $qb->getQuery()->getSingleScalarResult();
}
private $_cacheQuery = array();
/**
*
* @param array $terms
* @return \Doctrine\ORM\QueryBuilder
*/
public function createQuery(array $terms)
{
//get from cache
$cacheKey = md5(serialize($terms));
if (array_key_exists($cacheKey, $this->_cacheQuery)) {
return clone $this->_cacheQuery[$cacheKey];
}
$qb = $this->em->createQueryBuilder();
$qb->from('ChillPersonBundle:Person', 'p');
if (array_key_exists('firstname', $terms)) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.firstName))', ':firstname'))
->setParameter('firstname', '%'.$terms['firstname'].'%');
}
if (array_key_exists('lastname', $terms)) {
$qb->andWhere($qb->expr()->like('UNACCENT(LOWER(p.lastName))', ':lastname'))
->setParameter('lastname', '%'.$terms['lastname'].'%');
}
foreach (['birthdate', 'birthdate-before', 'birthdate-after'] as $key)
if (array_key_exists($key, $terms)) {
try {
$date = new \DateTime($terms[$key]);
} catch (\Exception $ex) {
throw new ParsingException('The date is '
. 'not parsable', 0, $ex);
}
switch($key) {
case 'birthdate':
$qb->andWhere($qb->expr()->eq('p.birthdate', ':birthdate'))
->setParameter('birthdate', $date);
break;
case 'birthdate-before':
$qb->andWhere($qb->expr()->lt('p.birthdate', ':birthdatebefore'))
->setParameter('birthdatebefore', $date);
break;
case 'birthdate-after':
$qb->andWhere($qb->expr()->gt('p.birthdate', ':birthdateafter'))
->setParameter('birthdateafter', $date);
break;
default:
throw new \LogicException("this case $key should not exists");
}
}
if (array_key_exists('gender', $terms)) {
if (!in_array($terms['gender'], array(Person::MALE_GENDER, Person::FEMALE_GENDER))) {
throw new ParsingException('The gender '
.$terms['gender'].' is not accepted. Should be "'.Person::MALE_GENDER
.'" or "'.Person::FEMALE_GENDER.'"');
}
$qb->andWhere($qb->expr()->eq('p.gender', ':gender'))
->setParameter('gender', $terms['gender']);
}
if (array_key_exists('nationality', $terms)) {
try {
$country = $this->em->createQuery('SELECT c FROM '
. 'ChillMainBundle:Country c WHERE '
. 'LOWER(c.countryCode) LIKE :code')
->setParameter('code', $terms['nationality'])
->getSingleResult();
} catch (\Doctrine\ORM\NoResultException $ex) {
throw new ParsingException('The country code "'.$terms['nationality'].'" '
. ', used in nationality, is unknow', 0, $ex);
}
$qb->andWhere($qb->expr()->eq('p.nationality', ':nationality'))
->setParameter('nationality', $country);
}
if ($terms['_default'] !== '') {
$grams = explode(' ', $terms['_default']);
foreach($grams as $key => $gram) {
$qb->andWhere($qb->expr()
->like('p.fullnameCanonical', 'UNACCENT(LOWER(:default_'.$key.'))'))
->setParameter('default_'.$key, '%'.$gram.'%');
foreach (['birthdateBefore', 'birthdateAfter', 'birthdate'] as $v) {
if (NULL !== ${$v}) {
try {
${$v} = new \DateTime(${$v});
} catch (\Exception $e) {
throw new ParsingException('The date is '
. 'not parsable', 0, $e);
}
}
}
//restraint center for security
$reachableCenters = $this->helper->getReachableCenters($this->user,
new Role('CHILL_PERSON_SEE'));
$qb->andWhere($qb->expr()
->in('p.center', ':centers'))
->setParameter('centers', $reachableCenters)
;
$this->_cacheQuery[$cacheKey] = $qb;
return clone $qb;
return $this->personACLAwareRepository
->countBySearchCriteria(
$default,
$firstname,
$lastname,
$birthdate,
$birthdateBefore,
$birthdateAfter,
$gender,
$countryCode,
);
}
public function buildForm(FormBuilderInterface $builder)
@@ -391,4 +289,10 @@ class PersonSearch extends AbstractSearch implements ContainerAwareInterface,
return 'Search within persons';
}
public static function getAlias(): string
{
return self::NAME;
}
}

View File

@@ -2,90 +2,32 @@
namespace Chill\PersonBundle\Search;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Search\AbstractSearch;
use Chill\MainBundle\Search\SearchInterface;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface;
use Symfony\Component\Templating\EngineInterface;
/**
* Class SimilarityPersonSearch
*
* @package Chill\PersonBundle\Search
*/
class SimilarityPersonSearch extends AbstractSearch
{
use ContainerAwareTrait;
/**
*
* @var EntityManagerInterface
*/
private $em;
/**
*
* @var \Chill\MainBundle\Entity\User
*/
private $user;
/**
*
* @var AuthorizationHelper
*/
private $helper;
/**
*
* @var PaginatorFactory
*/
protected $paginatorFactory;
protected PaginatorFactory $paginatorFactory;
private PersonACLAwareRepositoryInterface $personACLAwareRepository;
private EngineInterface $templating;
const NAME = "person_similarity";
/**
*
* @var PersonSearch
*/
private $personSearch;
/**
* SimilarityPersonSearch constructor.
*
* @param EntityManagerInterface $em
* @param TokenStorageInterface $tokenStorage
* @param AuthorizationHelper $helper
* @param PaginatorFactory $paginatorFactory
* @param PersonSearch $personSearch
*/
public function __construct(
EntityManagerInterface $em,
TokenStorageInterface $tokenStorage,
AuthorizationHelper $helper,
PaginatorFactory $paginatorFactory,
PersonSearch $personSearch)
{
$this->em = $em;
$this->user = $tokenStorage->getToken()->getUser();
$this->helper = $helper;
PersonACLAwareRepositoryInterface $personACLAwareRepository,
EngineInterface $templating
) {
$this->paginatorFactory = $paginatorFactory;
$this->personSearch = $personSearch;
// throw an error if user is not a valid user
if (!$this->user instanceof \Chill\MainBundle\Entity\User) {
throw new \LogicException('The user provided must be an instance'
. ' of Chill\MainBundle\Entity\User');
}
$this->personACLAwareRepository = $personACLAwareRepository;
$this->templating = $templating;
}
/*
* (non-PHPdoc)
* @see \Chill\MainBundle\Search\SearchInterface::getOrder()
@@ -94,7 +36,7 @@ class SimilarityPersonSearch extends AbstractSearch
{
return 200;
}
/*
* (non-PHPdoc)
* @see \Chill\MainBundle\Search\SearchInterface::isActiveByDefault()
@@ -103,30 +45,27 @@ class SimilarityPersonSearch extends AbstractSearch
{
return true;
}
public function supports($domain, $format)
{
return 'person' === $domain;
}
/**
* @param array $terms
* @param int $start
* @param int $limit
* @param array $options
* @param string $format
* @return array
*/
public function renderResult(array $terms, $start = 0, $limit = 50, array $options = array(), $format = 'html')
{
$total = $this->count($terms);
$paginator = $this->paginatorFactory->create($total);
if ($format === 'html')
{
if ($total !== 0)
{
return $this->container->get('templating')->render('ChillPersonBundle:Person:list.html.twig',
if ($format === 'html') {
if ($total !== 0) {
return $this->templating->render('ChillPersonBundle:Person:list.html.twig',
array(
'persons' => $this->search($terms, $start, $limit, $options),
'pattern' => $this->recomposePattern($terms, array('nationality',
@@ -143,9 +82,8 @@ class SimilarityPersonSearch extends AbstractSearch
else {
return null;
}
} elseif ($format === 'json')
{
} elseif ($format === 'json') {
return [
'results' => $this->search($terms, $start, $limit, \array_merge($options, [ 'simplify' => true ])),
'pagination' => [
@@ -154,8 +92,7 @@ class SimilarityPersonSearch extends AbstractSearch
];
}
}
/**
*
* @param string $pattern
@@ -166,101 +103,12 @@ class SimilarityPersonSearch extends AbstractSearch
*/
protected function search(array $terms, $start, $limit, array $options = array())
{
$qb = $this->createQuery($terms, 'search');
if ($options['simplify'] ?? false) {
$qb->select(
'sp.id',
$qb->expr()->concat(
'sp.firstName',
$qb->expr()->literal(' '),
'sp.lastName'
).'AS text'
);
} else {
$qb->select('sp');
}
$qb
->setMaxResults($limit)
->setFirstResult($start);
//order by firstname, lastname
$qb
->orderBy('sp.firstName')
->addOrderBy('sp.lastName');
if ($options['simplify'] ?? false) {
return $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
} else {
return $qb->getQuery()->getResult();
}
return $this->personACLAwareRepository
->findBySimilaritySearch($terms['_default'], $start, $limit, $options['simplify'] ?? false);
}
protected function count(array $terms)
{
$qb = $this->createQuery($terms);
$qb->select('COUNT(sp.id)');
return $qb->getQuery()->getSingleScalarResult();
return $this->personACLAwareRepository->countBySimilaritySearch($terms['_default']);
}
private $_cacheQuery = array();
/**
*
* @param array $terms
* @return \Doctrine\ORM\QueryBuilder
*/
protected function createQuery(array $terms)
{
//get from cache
$cacheKey = md5(serialize($terms));
if (array_key_exists($cacheKey, $this->_cacheQuery)) {
return clone $this->_cacheQuery[$cacheKey];
}
$qb = $this->em->createQueryBuilder();
$qb ->select('sp')
->from('ChillPersonBundle:Person', 'sp');
if ($terms['_default'] !== '') {
$grams = explode(' ', $terms['_default']);
foreach($grams as $key => $gram) {
$qb->andWhere('SIMILARITY(sp.fullnameCanonical, UNACCENT(LOWER(:default_'.$key.')) ) >= 0.15')
->setParameter('default_'.$key, '%'.$gram.'%');
}
$qb->andWhere($qb->expr()
->notIn(
'sp.id',
$this->personSearch
->createQuery($terms)
->addSelect('p.id')
->getDQL()
)
);
}
//restraint center for security
$reachableCenters = $this->helper->getReachableCenters($this->user,
new Role('CHILL_PERSON_SEE'));
$qb->andWhere($qb->expr()
->in('sp.center', ':centers'))
->setParameter('centers', $reachableCenters)
;
$this->_cacheQuery[$cacheKey] = $qb;
return clone $qb;
}
}
}

View File

@@ -4,73 +4,101 @@ namespace Chill\PersonBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Entity\Center;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
class AccompanyingPeriodVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
protected AuthorizationHelper $helper;
public const SEE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE';
/**
* details are for seeing:
*
* * SocialIssues
*/
public const SEE_DETAILS = 'CHILL_PERSON_ACCOMPANYING_PERIOD_SEE_DETAILS';
public const CREATE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE';
public const EDIT = 'CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE';
public const DELETE = 'CHILL_PERSON_ACCOMPANYING_PERIOD_DELETE';
/**
* @param AuthorizationHelper $helper
* Give all the right above
*/
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
public const FULL = 'CHILL_PERSON_ACCOMPANYING_PERIOD_FULL';
public const ALL = [
self::SEE,
self::SEE_DETAILS,
self::CREATE,
self::EDIT,
self::DELETE,
self::FULL,
];
private VoterHelperInterface $voterHelper;
private Security $security;
public function __construct(
Security $security,
VoterHelperFactoryInterface $voterHelperFactory
) {
$this->security = $security;
$this->voterHelper = $voterHelperFactory
->generate(self::class)
->addCheckFor(null, [self::CREATE])
->addCheckFor(AccompanyingPeriod::class, self::ALL)
->addCheckFor(Person::class, [self::SEE])
->build();
}
protected function supports($attribute, $subject)
{
return $subject instanceof AccompanyingPeriod;
return $this->voterHelper->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
if (!$token->getUser() instanceof User) {
return false;
}
// TODO take scopes into account
if (count($subject->getPersons()) === 0) {
return true;
}
foreach ($subject->getPersons() as $person) {
// give access as soon as on center is reachable
if ($this->helper->userHasAccess($token->getUser(), $person->getCenter(), $attribute)) {
return true;
if ($subject instanceof AccompanyingPeriod) {
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
// only creator can see, edit, delete, etc.
if ($subject->getCreatedBy() === $token->getUser()
|| NULL === $subject->getCreatedBy()) {
return true;
}
return false;
}
return false;
// if confidential, only the referent can see it
if ($subject->isConfidential()) {
return $token->getUser() === $subject->getUser();
}
}
}
private function getAttributes()
{
return [
self::SEE
];
return $this->voterHelper->voteOnAttribute($attribute, $subject, $token);
}
public function getRoles()
{
return $this->getAttributes();
return self::ALL;
}
public function getRolesWithoutScope()
{
return [];
}
public function getRolesWithHierarchy()
{
return [ 'Person' => $this->getRoles() ];
return [ 'Accompanying period' => $this->getRoles() ];
}
}

View File

@@ -22,17 +22,15 @@ namespace Chill\PersonBundle\Security\Authorization;
use Chill\MainBundle\Security\Authorization\AbstractChillVoter;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface;
use Chill\MainBundle\Security\Authorization\VoterHelperInterface;
use Chill\MainBundle\Security\ProvideRoleHierarchyInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcher;
use Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Entity\Center;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Role\Role;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInterface
{
const CREATE = 'CHILL_PERSON_CREATE';
@@ -41,51 +39,31 @@ class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte
const STATS = 'CHILL_PERSON_STATS';
const LISTS = 'CHILL_PERSON_LISTS';
const DUPLICATE = 'CHILL_PERSON_DUPLICATE';
/**
*
* @var AuthorizationHelper
*/
protected $helper;
public function __construct(AuthorizationHelper $helper)
{
$this->helper = $helper;
protected VoterHelperInterface $voter;
public function __construct(
VoterHelperFactoryInterface $voterFactory
) {
$this->voter = $voterFactory
->generate(self::class)
->addCheckFor(Center::class, [self::STATS, self::LISTS, self::DUPLICATE])
->addCheckFor(Person::class, [self::CREATE, self::UPDATE, self::SEE, self::DUPLICATE])
->addCheckFor(null, [self::CREATE] )
->build()
;
}
protected function supports($attribute, $subject)
{
if ($subject instanceof Person) {
return \in_array($attribute, [
self::CREATE, self::UPDATE, self::SEE, self::DUPLICATE
]);
} elseif ($subject instanceof Center) {
return \in_array($attribute, [
self::STATS, self::LISTS, self::DUPLICATE
]);
} elseif ($subject === null) {
return $attribute === self::CREATE;
} else {
return false;
}
return $this->voter->supports($attribute, $subject);
}
protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
{
if (!$token->getUser() instanceof User) {
return false;
}
if ($subject === null) {
$centers = $this->helper->getReachableCenters($token->getUser(),
new Role($attribute));
return count($centers) > 0;
}
return $this->helper->userHasAccess($token->getUser(), $subject, $attribute);
return $this->voter->voteOnAttribute($attribute, $subject, $token);
}
private function getAttributes()
{
return array(self::CREATE, self::UPDATE, self::SEE, self::STATS, self::LISTS, self::DUPLICATE);
@@ -100,7 +78,7 @@ class PersonVoter extends AbstractChillVoter implements ProvideRoleHierarchyInte
{
return $this->getAttributes();
}
public function getRolesWithHierarchy()
{
return [ 'Person' => $this->getRoles() ];

View File

@@ -19,6 +19,7 @@
namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
@@ -61,6 +62,9 @@ class PersonNormalizer implements
public function normalize($person, string $format = null, array $context = array())
{
/** @var Household $household */
$household = $person->getCurrentHousehold();
/** @var Person $person */
return [
'type' => 'person',
@@ -69,6 +73,7 @@ class PersonNormalizer implements
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'birthdate' => $this->normalizer->normalize($person->getBirthdate()),
'deathdate' => $this->normalizer->normalize($person->getDeathdate()),
'center' => $this->normalizer->normalize($person->getCenter()),
'phonenumber' => $person->getPhonenumber(),
'mobilenumber' => $person->getMobilenumber(),
@@ -76,6 +81,7 @@ class PersonNormalizer implements
'gender' => $person->getGender(),
'gender_numeric' => $person->getGenderNumeric(),
'current_household_address' => $this->normalizer->normalize($person->getCurrentHouseholdAddress()),
'current_household_id' => $household ? $this->normalizer->normalize($household->getId()) : null,
];
}
@@ -89,7 +95,7 @@ class PersonNormalizer implements
return $r;
}
public function supportsNormalization($data, string $format = null): bool
{
@@ -125,6 +131,7 @@ class PersonNormalizer implements
foreach ([
'birthdate' => \DateTime::class,
'deathdate' => \DateTime::class,
'center' => Center::class
] as $item => $class) {
if (\array_key_exists($item, $data)) {

View File

@@ -62,6 +62,7 @@ class PersonRender extends AbstractChillEntityRender
'address_multiline' => $options['address_multiline'] ?? false,
'hLevel' => $options['hLevel'] ?? 3,
'customButtons' => $options['customButtons'] ?? [],
'customArea' => $options['customArea'] ?? [],
];
return

View File

@@ -23,6 +23,7 @@
namespace Chill\PersonBundle\Tests\Controller;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
@@ -47,6 +48,8 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
protected ?AccompanyingPeriod $period = NULL;
protected ?int $periodId = NULL;
/**
* Setup before the first test of this class (see phpunit doc)
*/
@@ -70,15 +73,15 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
*
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testAccompanyingCourseShow(int $personId, AccompanyingPeriod $period)
public function testAccompanyingCourseShow(int $personId, int $periodId)
{
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()));
$c = $this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
$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(),
$this->assertEquals($data->id, $periodId,
"test that the response's data contains the id of the period"
);
$this->assertGreaterThan(0, $data->participations);
@@ -326,14 +329,16 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
*
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testAccompanyingPeriodPatch(int $personId, AccompanyingPeriod $period)
public function testAccompanyingPeriodPatch(int $personId, int $periodId)
{
$period = self::$container->get(AccompanyingPeriodRepository::class)
->find($periodId);
$initialValueEmergency = $period->isEmergency();
$em = self::$container->get(EntityManagerInterface::class);
$this->client->request(
Request::METHOD_PATCH,
sprintf('/api/1.0/person/accompanying-course/%d.json', $period->getId()),
sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId),
[], // parameters
[], // files
[], // server parameters
@@ -343,11 +348,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertEquals(200, $response->getStatusCode());
$period = $em->getRepository(AccompanyingPeriod::class)
->find($period->getId());
$em->refresh($period);
->find($periodId);
$this->assertEquals(!$initialValueEmergency, $period->isEmergency());
// restore the initial valud
// restore the initial value
$period->setEmergency($initialValueEmergency);
$em->flush();
}
@@ -361,12 +365,32 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$em = static::$container->get(EntityManagerInterface::class);
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$qb = $em->createQueryBuilder();
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
$personIds = $qb
->select('p.id')
->distinct(true)
->from(Person::class, 'p')
->join('p.accompanyingPeriodParticipations', 'participation')
->join('participation.accompanyingPeriod', 'ap')
->where(
$qb->expr()->eq(
'p.center',
':center'
)
)
->andWhere(
$qb->expr()->gt(
'SIZE(p.accompanyingPeriodParticipations)',
0)
)
->andWhere(
$qb->expr()->eq('ap.step', ':step')
)
->setParameter('center', $center)
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
// create a random order
@@ -400,11 +424,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
*
* @dataProvider dataGenerateRandomAccompanyingCourse
*/
public function testAccompanyingCourseAddParticipation(int $personId, AccompanyingPeriod $period)
public function testAccompanyingCourseAddParticipation(int $personId, int $periodId)
{
$this->client->request(
Request::METHOD_POST,
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
[], // parameters
[], // files
[], // server parameters
@@ -420,7 +444,8 @@ 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()));
$this->client->request(Request::METHOD_GET, sprintf('/api/1.0/person/accompanying-course/%d.json', $periodId));
$this->assertEquals(200, $response->getStatusCode(), "Test that the response of rest api has a status code ok (200)");
$response = $this->client->getResponse();
$data = \json_decode($response->getContent());
@@ -435,7 +460,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
// check removing the participation
$this->client->request(
Request::METHOD_DELETE,
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $period->getId()),
sprintf('/api/1.0/person/accompanying-course/%d/participation.json', $periodId),
[], // parameters
[], // files
[], // server parameters
@@ -450,42 +475,6 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$this->assertNotNull($data['startDate']);
$this->assertArrayHasKey('endDate', $data);
$this->assertNotNull($data['endDate']);
// set to variable for tear down
$this->personId = $personId;
$this->period = $period;
}
protected function tearDown()
{
$em = static::$container->get(EntityManagerInterface::class);
// remove participation if set
if ($this->personId && $this->period) {
$participation = $em
->getRepository(AccompanyingPeriodParticipation::class)
->findOneBy(['person' => $this->personId, 'accompanyingPeriod' => $this->period])
;
if (NULL !== $participation) {
$em->remove($participation);
$em->flush();
}
$this->personId = NULL;
$this->period = NULL;
} elseif ($this->period) {
$period = $em
->getRepository(AccompanyingPeriod::class)
->find($this->period->getId()) ;
if ($period !== NULL) {
$em->remove($period);
$em->flush();
}
$this->period = null;
}
}
public function dataGenerateRandomAccompanyingCourseWithSocialIssue()
@@ -505,12 +494,32 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$em = static::$container->get(EntityManagerInterface::class);
$center = $em->getRepository(Center::class)
->findOneBy(array('name' => 'Center A'));
$qb = $em->createQueryBuilder();
$personIds = $em->createQuery("SELECT p.id FROM ".
Person::class." p ".
" WHERE p.center = :center")
$personIds = $qb
->select('p.id')
->distinct(true)
->from(Person::class, 'p')
->join('p.accompanyingPeriodParticipations', 'participation')
->join('participation.accompanyingPeriod', 'ap')
->where(
$qb->expr()->eq(
'p.center',
':center'
)
)
->andWhere(
$qb->expr()->gt(
'SIZE(p.accompanyingPeriodParticipations)',
0)
)
->andWhere(
$qb->expr()->eq('ap.step', ':step')
)
->setParameter('center', $center)
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
// create a random order
@@ -556,7 +565,10 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
$qb = $em->createQueryBuilder();
$personIds = $qb
->select('p.id')
->distinct(true)
->from(Person::class, 'p')
->join('p.accompanyingPeriodParticipations', 'participation')
->join('participation.accompanyingPeriod', 'ap')
->where(
$qb->expr()->eq(
'p.center',
@@ -568,7 +580,11 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
'SIZE(p.accompanyingPeriodParticipations)',
0)
)
->andWhere(
$qb->expr()->eq('ap.step', ':step')
)
->setParameter('center', $center)
->setParameter('step', AccompanyingPeriod::STEP_CONFIRMED)
->setMaxResults($maxResults)
->getQuery()
->getScalarResult();
@@ -584,7 +600,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase
->find($id);
$periods = $person->getAccompanyingPeriods();
yield [\array_pop($personIds)["id"], $periods[\array_rand($periods)] ];
yield [\array_pop($personIds)["id"], $periods[\array_rand($periods)]->getId() ];
$nbGenerated++;
}

View File

@@ -1,112 +0,0 @@
<?php
/*
* Copyright (C) 2015 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/>.
*/
namespace Chill\PersonBundle\Tests\Validator;
use Chill\PersonBundle\Validator\Constraints\BirthdateValidator;
use Chill\PersonBundle\Validator\Constraints\Birthdate;
use Prophecy\Argument;
use Prophecy\Prophet;
/**
* Test the behaviour of BirthdayValidator
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class BirthdateValidatorTest extends \PHPUnit\Framework\TestCase
{
/**
* A prophecy for \Symfony\Component\Validator\Context\ExecutionContextInterface
*
* Will reveal \Symfony\Component\Validator\Context\ExecutionContextInterface
*/
private $context;
private $prophet;
/**
*
* @var Birthdate
*/
private $constraint;
public function setUp()
{
$this->prophet = new Prophet;
$constraintViolationBuilder = $this->prophet->prophesize();
$constraintViolationBuilder->willImplement('Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface');
$constraintViolationBuilder->setParameter(Argument::any(), Argument::any())
->willReturn($constraintViolationBuilder->reveal());
$constraintViolationBuilder->addViolation()
->willReturn($constraintViolationBuilder->reveal());
$this->context = $this->prophet->prophesize();
$this->context->willImplement('Symfony\Component\Validator\Context\ExecutionContextInterface');
$this->context->buildViolation(Argument::type('string'))
->willReturn($constraintViolationBuilder->reveal());
$this->constraint = new Birthdate();
}
public function testValidBirthDate()
{
$date = new \DateTime('2015-01-01');
$birthdateValidator = new BirthdateValidator();
$birthdateValidator->initialize($this->context->reveal());
$birthdateValidator->validate($date, $this->constraint);
$this->context->buildViolation(Argument::any())->shouldNotHaveBeenCalled();
}
public function testInvalidBirthDate()
{
$date = new \DateTime('tomorrow');
$birthdateValidator = new BirthdateValidator();
$birthdateValidator->initialize($this->context->reveal());
$birthdateValidator->validate($date, $this->constraint);
$this->context->buildViolation(Argument::type('string'))->shouldHaveBeenCalled();
}
public function testInvalidBirthDateWithParameter()
{
$date = (new \DateTime('today'))->sub(new \DateInterval('P1M'));
$birthdateValidator = new BirthdateValidator('P1Y');
$birthdateValidator->initialize($this->context->reveal());
$birthdateValidator->validate($date, $this->constraint);
$this->context->buildViolation(Argument::type('string'))->shouldHaveBeenCalled();
}
public function tearDown()
{
$this->prophet->checkPredictions();
}
}

View File

@@ -0,0 +1,74 @@
<?php
/*
* Copyright (C) 2015 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/>.
*/
namespace Chill\PersonBundle\Tests\Validator\Person;
use Chill\PersonBundle\Validator\Constraints\Person\BirthdateValidator;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Symfony\Component\Validator\Test\ConstraintValidatorTestCase;
use Chill\PersonBundle\Entity\Person;
/**
* Test the behaviour of BirthdayValidator
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class BirthdateValidatorTest extends ConstraintValidatorTestCase
{
public function testValidateTodayInvalid()
{
$bornToday = new \DateTime('now');
$this->validator->validate($bornToday, $this->createConstraint());
$this->buildViolation('msg')
->setParameter('%date%', (new \DateTime('yesterday'))->format('d-m-Y'))
->setCode('3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4')
->assertRaised();
}
public function testValidateYesterdayValid()
{
$bornYesterday = new \DateTime('yesterday');
$this->validator->validate($bornYesterday, $this->createConstraint());
$this->assertNoViolation();
}
public function testTomorrowInvalid()
{
$bornAfter = new \DateTime('+2 days');
$this->validator->validate($bornAfter, $this->createConstraint());
$this->buildViolation('msg')
->setParameter('%date%', (new \DateTime('yesterday'))->format('d-m-Y'))
->setCode('3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4')
->assertRaised();
}
private function createConstraint()
{
return new Birthdate([
'message' => 'msg'
]);
}
protected function createValidator()
{
return new BirthdateValidator('P1D');
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Validator\Person;
use Chill\MainBundle\Entity\Center;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenterValidator;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class PersonHasCenterValidatorTest extends \Symfony\Component\Validator\Test\ConstraintValidatorTestCase
{
public function testValidateRequired()
{
$constraint = $this->getConstraint();
$personHasCenter = (new Person())->setCenter(new Center());
$personNoCenter = new Person();
$this->validator->validate($personHasCenter, $constraint);
$this->assertNoViolation();
$this->validator->validate($personNoCenter, $constraint);
$this->buildViolation('msg')
->atPath('property.path.center')
->assertRaised();
}
protected function getConstraint()
{
return new PersonHasCenter([
'message' => 'msg'
]);
}
protected function createValidator()
{
$parameterBag = $this->createMock(ParameterBagInterface::class);
$parameterBag
->method('get')
->with($this->equalTo('chill_person'))
->willReturn([
'validation' => [
'center_required' => true
]
])
;
return new PersonHasCenterValidator($parameterBag);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Validator\Person;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
class PersonValidationTest extends KernelTestCase
{
private ValidatorInterface $validator;
protected function setUp()
{
self::bootKernel();
$this->validator = self::$container->get(ValidatorInterface::class);
}
public function testFirstnameValidation()
{
$person = (new Person())
->setFirstname(\str_repeat('a', 500));
$errors = $this->validator->validate($person, null, ["creation"]);
foreach ($errors->getIterator() as $error) {
if (Length::TOO_LONG_ERROR === $error->getCode()) {
$this->assertTrue(true,
"error code for firstname too long is present");
return;
}
}
$this->assertTrue(false,
"error code for fistname too long is present");
}
public function testBirthdateInFuture()
{
$person = (new Person())
->setBirthdate(new \Datetime('+2 months'));
$errors = $this->validator->validate($person, null, ["creation"]);
foreach ($errors->getIterator() as $error) {
if (Birthdate::BIRTHDATE_INVALID_CODE === $error->getCode()) {
$this->assertTrue(true,
"error code for birthdate invalid is present");
return;
}
}
$this->assertTrue(false,
"error code for birthdate invalid is present");
}
}

View File

@@ -17,26 +17,23 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\Validator\Constraints;
namespace Chill\PersonBundle\Validator\Constraints\Person;
use Symfony\Component\Validator\Constraint;
/**
* Create a constraint on birth date: the birthdate after today are not allowed.
*
* It is possible to add a delay before today, expressed as described in
*
* It is possible to add a delay before today, expressed as described in
* interval_spec : http://php.net/manual/en/dateinterval.construct.php
* (this interval_spec itself is based on ISO8601 :
* https://en.wikipedia.org/wiki/ISO_8601#Durations)
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
* @Annotation
*/
class Birthdate extends Constraint
{
public const BIRTHDATE_INVALID_CODE = '3f42fd96-0b2d-11ec-8cf3-0f3b1b1ca1c4';
public $message = "The birthdate must be before %date%";
public function validatedBy()
{
return 'birthdate_not_before';
}
}

View File

@@ -17,49 +17,50 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Chill\PersonBundle\Validator\Constraints;
namespace Chill\PersonBundle\Validator\Constraints\Person;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
*
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class BirthdateValidator extends ConstraintValidator
{
private $interval_spec = null;
public function __construct($interval_spec = null)
{
$this->interval_spec = $interval_spec;
}
public function validate($value, Constraint $constraint)
{
if ($value === NULL) {
return;
}
if (!$value instanceof \DateTime) {
throw new \LogicException('The input should a be a \DateTime interface,'
. (is_object($value) ? get_class($value) : gettype($value)));
}
$limitDate = $this->getLimitDate();
if ($limitDate < $value) {
$this->context->buildViolation($constraint->message)
->setParameter('%date%', $limitDate->format('d-m-Y'))
->setCode(Birthdate::BIRTHDATE_INVALID_CODE)
->addViolation();
}
}
/**
*
*
* @return \DateTime
*/
private function getLimitDate()

View File

@@ -0,0 +1,18 @@
<?php
namespace Chill\PersonBundle\Validator\Constraints\Person;
/**
* @Annotation
*/
class PersonHasCenter extends \Symfony\Component\Validator\Constraint
{
public string $message = "A center is required";
public function getTargets()
{
return [
self::CLASS_CONSTRAINT
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Chill\PersonBundle\Validator\Constraints\Person;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
class PersonHasCenterValidator extends \Symfony\Component\Validator\ConstraintValidator
{
private bool $centerRequired;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->centerRequired = $parameterBag->get('chill_person')['validation']['center_required'];
}
/**
* @inheritDoc
*/
public function validate($person, Constraint $constraint)
{
if (!$person instanceof Person) {
throw new UnexpectedTypeException($constraint, Person::class);
}
if (!$this->centerRequired) {
return;
}
if (NULL === $person->getCenter()) {
$this
->context
->buildViolation($constraint->message)
->atPath('center')
->addViolation()
;
}
}
}

View File

@@ -45,7 +45,7 @@ components:
type:
type: string
enum:
- 'person'
- "person"
firstName:
type: string
lastName:
@@ -55,7 +55,9 @@ components:
description: a canonical representation for the person name
readOnly: true
birthdate:
$ref: '#/components/schemas/Date'
$ref: "#/components/schemas/Date"
deathdate:
$ref: "#/components/schemas/Date"
phonenumber:
type: string
mobilenumber:
@@ -78,7 +80,7 @@ components:
type:
type: string
enum:
- 'person'
- "person"
required:
- id
- type
@@ -96,7 +98,7 @@ components:
type:
type: string
enum:
- 'thirdparty'
- "thirdparty"
required:
- id
- type
@@ -119,15 +121,15 @@ components:
type:
type: string
enum:
- 'accompanying_period_resource'
- "accompanying_period_resource"
readOnly: true
id:
type: integer
readOnly: true
resource:
anyOf:
- $ref: '#/components/schemas/PersonById'
- $ref: '#/components/schemas/ThirdPartyById'
- $ref: "#/components/schemas/PersonById"
- $ref: "#/components/schemas/ThirdPartyById"
ResourceById:
type: object
properties:
@@ -136,7 +138,7 @@ components:
type:
type: string
enum:
- 'accompanying_period_resource'
- "accompanying_period_resource"
required:
- id
- type
@@ -146,7 +148,7 @@ components:
type:
type: string
enum:
- 'accompanying_period_comment'
- "accompanying_period_comment"
readOnly: true
id:
type: integer
@@ -161,7 +163,7 @@ components:
type:
type: string
enum:
- 'accompanying_period_comment'
- "accompanying_period_comment"
required:
- id
- type
@@ -173,7 +175,7 @@ components:
type:
type: string
enum:
- 'social_issue'
- "social_issue"
parent_id:
type: integer
readOnly: true
@@ -195,12 +197,12 @@ components:
Household:
type: object
properties:
id:
type: integer
type:
type: string
enum:
- 'household'
id:
type: integer
type:
type: string
enum:
- "household"
HouseholdPosition:
type: object
properties:
@@ -209,7 +211,7 @@ components:
type:
type: string
enum:
- 'household_position'
- "household_position"
AccompanyingCourseWork:
type: object
properties:
@@ -218,7 +220,7 @@ components:
type:
type: string
enum:
- 'accompanying_period_work'
- "accompanying_period_work"
note:
type: string
startDate:
@@ -243,15 +245,15 @@ components:
type:
type: string
enum:
- 'accompanying_period_work_goal'
- "accompanying_period_work_goal"
note:
type: string
goal:
$ref: '#/components/schemas/SocialWorkGoalById'
$ref: "#/components/schemas/SocialWorkGoalById"
results:
type: array
items:
$ref: '#/components/schemas/SocialWorkGoalById'
$ref: "#/components/schemas/SocialWorkGoalById"
SocialWorkResultById:
type: object
@@ -261,7 +263,7 @@ components:
type:
type: string
enum:
- 'social_work_result'
- "social_work_result"
SocialWorkGoalById:
type: object
properties:
@@ -270,7 +272,7 @@ components:
type:
type: string
enum:
- 'social_work_goal'
- "social_work_goal"
paths:
/1.0/person/person/{id}.json:
@@ -308,7 +310,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Person'
$ref: "#/components/schemas/Person"
responses:
200:
description: "OK"
@@ -355,18 +357,17 @@ paths:
422:
description: "Unprocessable entity (validation errors)"
/1.0/person/address/suggest/by-person/{id}.json:
get:
tags:
- address
summary: get a list of suggested address for a person
description: >
The address are computed from various source. Currently:
The address are computed from various source. Currently:
- the address of course to which the person is participating
- the address of course to which the person is participating
The current person's address is always ignored.
The current person's address is always ignored.
parameters:
- name: id
in: path
@@ -390,11 +391,11 @@ paths:
- address
summary: get a list of suggested address for a household
description: >
The address are computed from various source. Currently:
The address are computed from various source. Currently:
- the address of course to which the members is participating
- the address of course to which the members is participating
The current household address is always ignored.
The current household address is always ignored.
parameters:
- name: id
in: path
@@ -452,7 +453,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AccompanyingPeriod'
$ref: "#/components/schemas/AccompanyingPeriod"
examples:
Set the requestor as anonymous:
value:
@@ -488,7 +489,7 @@ paths:
id: 0
personLocation: null
addressLocation:
id: 7960
id: 7960
responses:
401:
description: "Unauthorized"
@@ -520,8 +521,8 @@ paths:
application/json:
schema:
oneOf:
- $ref: '#/components/schemas/PersonById'
- $ref: '#/components/schemas/ThirdPartyById'
- $ref: "#/components/schemas/PersonById"
- $ref: "#/components/schemas/ThirdPartyById"
examples:
add person with id 50:
summary: "a person with id 50"
@@ -585,7 +586,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/PersonById'
$ref: "#/components/schemas/PersonById"
responses:
401:
description: "Unauthorized"
@@ -614,7 +615,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/PersonById'
$ref: "#/components/schemas/PersonById"
responses:
401:
description: "Unauthorized"
@@ -645,7 +646,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Resource'
$ref: "#/components/schemas/Resource"
examples:
add person with id 50:
summary: "a person with id 50"
@@ -690,7 +691,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ResourceById'
$ref: "#/components/schemas/ResourceById"
responses:
401:
description: "Unauthorized"
@@ -721,7 +722,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Comment'
$ref: "#/components/schemas/Comment"
examples:
a single comment:
summary: "a simple comment"
@@ -759,7 +760,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/CommentById'
$ref: "#/components/schemas/CommentById"
responses:
401:
description: "Unauthorized"
@@ -790,7 +791,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Scope'
$ref: "#/components/schemas/Scope"
examples:
add a scope:
value:
@@ -824,7 +825,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/ScopeById'
$ref: "#/components/schemas/ScopeById"
responses:
401:
description: "Unauthorized"
@@ -855,7 +856,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SocialIssue'
$ref: "#/components/schemas/SocialIssue"
examples:
add a social issue:
value:
@@ -889,7 +890,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SocialIssue'
$ref: "#/components/schemas/SocialIssue"
responses:
401:
description: "Unauthorized"
@@ -926,11 +927,11 @@ paths:
type:
type: string
enum:
- 'accompanying_period_work'
- "accompanying_period_work"
startDate:
$ref: '#/components/schemas/Date'
$ref: "#/components/schemas/Date"
endDate:
$ref: '#/components/schemas/Date'
$ref: "#/components/schemas/Date"
examples:
create a work:
value:
@@ -992,7 +993,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/AccompanyingCourseWork'
$ref: "#/components/schemas/AccompanyingCourseWork"
responses:
401:
description: "Unauthorized"
@@ -1029,8 +1030,6 @@ paths:
400:
description: "transition cannot be applyed"
/1.0/person/accompanying-period/origin.json:
get:
tags:
@@ -1064,8 +1063,6 @@ paths:
404:
description: "Not found"
/1.0/person/household.json:
get:
tags:
@@ -1095,7 +1092,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Household'
$ref: "#/components/schemas/Household"
404:
description: "not found"
401:
@@ -1107,9 +1104,9 @@ paths:
- household
summary: Return households associated with the given person through accompanying periods
description: |
Return households associated with the given person throught accompanying periods participation.
Return households associated with the given person throught accompanying periods participation.
The current household of the given person is excluded.
The current household of the given person is excluded.
parameters:
- name: person_id
in: path
@@ -1125,7 +1122,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/Household'
$ref: "#/components/schemas/Household"
404:
description: "not found"
401:
@@ -1149,23 +1146,22 @@ paths:
type: object
properties:
person:
$ref: '#/components/schemas/PersonById'
$ref: "#/components/schemas/PersonById"
start_date:
$ref: '#/components/schemas/Date'
$ref: "#/components/schemas/Date"
position:
$ref: '#/components/schemas/HouseholdPosition'
$ref: "#/components/schemas/HouseholdPosition"
holder:
type: boolean
comment:
type: string
destination:
$ref: '#/components/schemas/Household'
$ref: "#/components/schemas/Household"
examples:
Moving person to a new household:
value:
concerned:
-
person:
- person:
id: 0
type: person
position:
@@ -1180,8 +1176,7 @@ paths:
Moving person to a new household and set an address to this household:
value:
concerned:
-
person:
- person:
id: 0
type: person
position:
@@ -1198,8 +1193,7 @@ paths:
Moving person to an existing household:
value:
concerned:
-
person:
- person:
id: 0
type: person
position:
@@ -1215,8 +1209,7 @@ paths:
Removing a person from any household:
value:
concerned:
-
person:
- person:
id: 0
type: person
start_date:
@@ -1270,8 +1263,6 @@ paths:
400:
description: "transition cannot be applyed"
/1.0/person/social/social-action.json:
get:
tags:
@@ -1349,7 +1340,6 @@ paths:
404:
description: not found
/1.0/person/social-work/social-issue.json:
get:
tags:
@@ -1379,7 +1369,7 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/SocialIssue'
$ref: "#/components/schemas/SocialIssue"
404:
description: "not found"
401:

View File

@@ -24,14 +24,6 @@ services:
tags:
- { name: console.command }
chill.person.form.type.select2maritalstatus:
class: Chill\PersonBundle\Form\Type\Select2MaritalStatusType
arguments:
- "@request_stack"
- "@doctrine.orm.entity_manager"
tags:
- { name: form.type, alias: select2_chill_marital_status }
chill.person.timeline.accompanying_period_opening:
class: Chill\PersonBundle\Timeline\TimelineAccompanyingPeriodOpening
arguments:
@@ -62,23 +54,22 @@ services:
- { name: security.voter }
- { name: chill.role }
chill.person.birthdate_validation:
class: Chill\PersonBundle\Validator\Constraints\BirthdateValidator
Chill\PersonBundle\Validator\Constraints\:
autowire: true
autoconfigure: true
resource: '../Validator/Constraints/'
# override default config, must be loaded after resource
Chill\PersonBundle\Validator\Constraints\BirthdateValidator:
arguments:
- "%chill_person.validation.birtdate_not_before%"
tags:
- { name: validator.constraint_validator, alias: birthdate_not_before }
Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequentialValidator:
autowire: true
tags:
- { name: validator.constraint_validator }
Chill\PersonBundle\Repository\:
autowire: true
autoconfigure: true
resource: '../Repository/'
tags: ['doctrine.repository_service']
Chill\PersonBundle\Controller\:
autowire: true

View File

@@ -12,9 +12,8 @@ services:
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\AccompanyingPeriodController:
arguments:
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
$validator: '@Symfony\Component\Validator\Validator\ValidatorInterface'
autowire: true
autoconfigure: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\PersonAddressController:

View File

@@ -1,5 +1,10 @@
services:
Chill\PersonBundle\Form\:
autowire: true
autoconfigure: true
resource: '../../Form/'
Chill\PersonBundle\Form\PersonType:
arguments:
- '%chill_person.person_fields%'
@@ -7,14 +12,7 @@ services:
tags:
- { name: form.type, alias: '@chill.person.form.person_creation' }
Chill\PersonBundle\Form\CreationPersonType:
arguments:
- '@chill.main.form.data_transformer.center_transformer'
- '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
tags:
- { name: form.type, alias: '@chill.main.form.person_creation' }
chill.person.accompanying_period_closing_motive:
Chill\PersonBundle\Form\Type\ClosingMotivePickerType:
class: Chill\PersonBundle\Form\Type\ClosingMotivePickerType
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
@@ -28,60 +26,3 @@ services:
$config: "%chill_person.accompanying_period_fields%"
tags:
- { name: form.type }
chill.person.form.type.pick_person:
class: Chill\PersonBundle\Form\Type\PickPersonType
arguments:
- '@Chill\PersonBundle\Repository\PersonRepository'
- "@security.token_storage"
- "@chill.main.security.authorization.helper"
- '@Symfony\Component\Routing\Generator\UrlGeneratorInterface'
- '@Symfony\Component\Translation\TranslatorInterface'
tags:
- { name: form.type }
Chill\PersonBundle\Form\Type\PersonAltNameType:
arguments:
$configHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }
Chill\PersonBundle\Form\Type\PersonPhoneType:
arguments:
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
$em: '@Doctrine\ORM\EntityManagerInterface'
tags:
- { name: form.type }
Chill\PersonBundle\Form\SocialWork\SocialIssueType:
arguments:
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }
Chill\PersonBundle\Form\SocialWork\SocialActionType:
arguments:
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }
Chill\PersonBundle\Form\SocialWork\EvaluationType:
arguments:
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }
Chill\PersonBundle\Form\SocialWork\GoalType:
arguments:
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }
Chill\PersonBundle\Form\SocialWork\ResultType:
arguments:
$translatableStringHelper: '@chill.main.helper.translatable_string'
tags:
- { name: form.type }

View File

@@ -19,14 +19,7 @@ services:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
arguments:
$showAccompanyingPeriod: '%chill_person.accompanying_period%'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
autowire: true
autoconfigure: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\AccompanyingCourseMenuBuilder:
# arguments:
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }

View File

@@ -1,6 +1,12 @@
services:
# note: the services.yaml file define some autoloading
chill.person.repository.person:
class: Chill\PersonBundle\Repository\PersonRepository
autowire: true
autoconfigure: true
Chill\PersonBundle\Repository\PersonRepository: '@chill.person.repository.person'
Chill\PersonBundle\Repository\PersonACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\PersonACLAwareRepository'
Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface: '@Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository'

View File

@@ -1,25 +1,11 @@
services:
chill.person.search_person:
class: Chill\PersonBundle\Search\PersonSearch
arguments:
- "@doctrine.orm.entity_manager"
- "@security.token_storage"
- "@chill.main.security.authorization.helper"
- "@chill_main.paginator_factory"
calls:
- ['setContainer', ["@service_container"]]
Chill\PersonBundle\Search\PersonSearch:
autowire: true
tags:
- { name: chill.search, alias: 'person_regular' }
Chill\PersonBundle\Search\SimilarityPersonSearch:
arguments:
- "@doctrine.orm.entity_manager"
- "@security.token_storage"
- "@chill.main.security.authorization.helper"
- "@chill_main.paginator_factory"
- '@chill.person.search_person'
calls:
- ['setContainer', ["@service_container"]]
autowire: true
tags:
- { name: chill.search, alias: 'person_similarity' }

View File

@@ -1,16 +1,14 @@
services:
chill.person.security.authorization.person:
autowire: true
class: Chill\PersonBundle\Security\Authorization\PersonVoter
arguments:
- "@chill.main.security.authorization.helper"
tags:
- { name: security.voter }
- { name: chill.role }
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter:
arguments:
- "@chill.main.security.authorization.helper"
autowire: true
tags:
- { name: security.voter }
- { name: chill.role }

View File

@@ -1,70 +1,3 @@
Chill\PersonBundle\Entity\Person:
properties:
firstName:
- NotBlank:
groups: [general, creation]
- Length:
min: 2
max: 255
minMessage: 'This name is too short. It must containt {{ limit }} chars'
maxMessage: 'This name is too long. It must containt {{ limit }} chars'
groups: [general, creation]
lastName:
- NotBlank:
groups: [general, creation]
- Length:
min: 2
max: 255
minMessage: 'This name is too short. It must containt {{ limit }} chars'
maxMessage: 'This name is too long. It must containt {{ limit }} chars'
groups: [general, creation]
birthdate:
- Date:
message: 'Birthdate not valid'
groups: [general, creation]
- Chill\PersonBundle\Validator\Constraints\Birthdate:
groups: [general, creation]
gender:
- NotNull:
groups: [general, creation]
#accompanyingPeriods:
# - Valid:
# traverse: true
email:
- Email:
groups: [general, creation]
message: 'The email is not valid'
checkMX: true
phonenumber:
- Regex:
pattern: '/^([\+{1}])([0-9\s*]{4,20})$/'
groups: [general, creation]
message: 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33123456789'
- Chill\MainBundle\Validation\Constraint\PhonenumberConstraint:
type: landline
groups: [ general, creation ]
mobilenumber:
- Regex:
pattern: '/^([\+{1}])([0-9\s*]{4,20})$/'
groups: [general, creation]
message: 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33623456789'
- Chill\MainBundle\Validation\Constraint\PhonenumberConstraint:
type: mobile
groups: [ general, creation ]
otherPhoneNumbers:
- Valid:
traverse: true
constraints:
- Callback:
callback: isAccompanyingPeriodValid
groups: [accompanying_period_consistent]
- Callback:
callback: isAddressesValid
groups: [addresses_consistent]
- Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential:
groups: [ 'household_memberships' ]
Chill\PersonBundle\Entity\AccompanyingPeriod:
properties:
openingDate:

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Allow to create persons without center
*/
final class Version20210831140339 extends AbstractMigration
{
public function getDescription(): string
{
return 'Allow to create persons without center';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_person ALTER center_id DROP NOT NULL');
}
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_person ALTER center_id SET NOT NULL');
}
}

View File

@@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Optimize trigram index on person fullname: create index for both center_id and fullname
*/
final class Version20210910161858 extends AbstractMigration
{
public function getDescription(): string
{
return 'Optimize trigram index on person fullname: create index for both center_id and fullname';
}
public function up(Schema $schema): void
{
$this->addSql('DROP INDEX fullnamecanonical_trgm_idx');
$this->addSql('CREATE INDEX fullnameCanonical_trgm_idx ON chill_person_person USING GIST (center_id, fullnameCanonical gist_trgm_ops)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX fullnamecanonical_trgm_idx');
$this->addSql('CREATE INDEX fullnameCanonical_trgm_idx ON chill_person_person USING GIST (fullnameCanonical gist_trgm_ops)');
}
}

Some files were not shown because too many files have changed in this diff Show More