Merge remote-tracking branch 'origin/master' into homepage/rewrite

This commit is contained in:
Julien Fastré 2022-01-28 14:13:28 +01:00
commit 2cd51eed2e
90 changed files with 2719 additions and 155 deletions

View File

@ -14,6 +14,26 @@ and this project adheres to
## Test releases
### test release 2021-01-28
* [person] improve filiations vis graph: disable physics, use chill colors for persons-households-course, increase label of relations, remove labels on household arrows and other improvements (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/286, https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/362)
* [activity] Order activity by date and by id (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/364)
* [main] increase length of 4 Address fields (change to TEXT, no size limits) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/277)
* [main] Add confidential option for address, in edit and view (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/165)
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
* [person] Add residential address entity, form and list for each person
* [aside_activity]: dynamicUserPickerType used (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/399)
### test release 2021-01-26
* [parcours] comments truncated if too long + link added (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/406)
* [person]: possibility to add person resources (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/382)
* [person ressources]: module added
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
* dispatching list
### test release 2022-01-24
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
@ -32,7 +52,6 @@ and this project adheres to
* add My workflow section with my opened subscriptions
* apply workflow on documents, accompanyingCourseWork and Evaluations
* [wopi-link] a new vue component allow to open wopi link in a fullscreen chill-themed modal
<!-- end -->
### test release 2022-01-19
* vuejs: add dead information on all on-the-fly person render boxes, in vis graph and other templates (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/271)
@ -40,6 +59,7 @@ and this project adheres to
* [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246)
* [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176)
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
* [forms] dynamic picker types for user/person/thirdparty types created (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/386)
### test release 2022-01-17
@ -49,10 +69,14 @@ and this project adheres to
* [main] Add mainLocation field to User entity and add it in user form type
* [course list in person context] show full username/label for ref
* [accompanying period work] remove the possibility to generate document from an accompanying period work
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
## Test releases
* vuejs: add validation on required fields for AddPerson, Address and Location components
* vuejs: treat 422 validation errors in locations and AddPerson components
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
### test release 2022-01-12

View File

@ -1,3 +0,0 @@
add npm/yarn dependency in package.json :
"select2-bootstrap-theme": "0.1.0-beta.10",

View File

@ -256,7 +256,7 @@ final class ActivityController extends AbstractController
if ($person instanceof Person) {
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $person);
$activities = $this->activityACLAwareRepository
->findByPerson($person, ActivityVoter::SEE, 0, null);
->findByPerson($person, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
$event = new PrivacyEvent($person, [
'element_class' => Activity::class,
@ -269,7 +269,7 @@ final class ActivityController extends AbstractController
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $accompanyingPeriod);
$activities = $this->activityACLAwareRepository
->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE);
->findByAccompanyingPeriod($accompanyingPeriod, ActivityVoter::SEE, 0, null, ['date' => 'DESC', 'id' => 'DESC']);
$view = 'ChillActivityBundle:Activity:listAccompanyingCourse.html.twig';
}

View File

@ -61,7 +61,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod")
* @Groups({"read", "docgen:read"})
* @Groups({"read"})
*/
private ?AccompanyingPeriod $accompanyingPeriod = null;
@ -75,16 +75,19 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityPresence")
* @Groups({"docgen:read"})
*/
private ?ActivityPresence $attendee = null;
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
* @Groups({"docgen:read"})
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="datetime")
* @Groups({"docgen:read"})
*/
private DateTime $date;
@ -100,6 +103,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* @ORM\Column(type="boolean", options={"default": false})
* @Groups({"docgen:read"})
*/
private bool $emergency = false;
@ -130,16 +134,19 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* @ORM\ManyToMany(targetEntity="Chill\ActivityBundle\Entity\ActivityReason")
* @Groups({"docgen:read"})
*/
private Collection $reasons;
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Scope")
* @Groups({"docgen:read"})
*/
private ?Scope $scope = null;
/**
* @ORM\Column(type="string", options={"default": ""})
* @Groups({"docgen:read"})
*/
private string $sentReceived = '';
@ -170,12 +177,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
/**
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"docgen:read"})
*/
private User $user;
/**
* @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\User")
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?Collection $users = null;
@ -302,6 +310,18 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this->documents;
}
/**
* @Groups({"docgen:read"})
*/
public function getDurationMinute(): int
{
if (null === $this->durationTime) {
return 0;
}
return (int) round(($this->durationTime->getTimestamp() + $this->durationTime->getOffset()) / 60.0, 0);
}
public function getDurationTime(): ?DateTime
{
return $this->durationTime;
@ -402,6 +422,18 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
return $this->travelTime;
}
/**
* @Groups({"docgen:read"})
*/
public function getTravelTimeMinute(): int
{
if (null === $this->travelTime) {
return 0;
}
return (int) round(($this->travelTime->getTimestamp() + $this->travelTime->getOffset()) / 60.0, 0);
}
/**
* @deprecated
*/

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* Class ActivityPresence.
@ -31,11 +32,14 @@ class ActivityPresence
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @Serializer\Groups({"docgen:read"})
*/
private ?int $id;
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"docgen:read"})
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
*/
private array $name = [];

View File

@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use InvalidArgumentException;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
@ -118,6 +119,7 @@ class ActivityType
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
* @Groups({"docgen:read"})
*/
private ?int $id;
@ -133,7 +135,8 @@ class ActivityType
/**
* @ORM\Column(type="json")
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
*/
private array $name = [];

View File

@ -29,9 +29,13 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
final class ActivityVoterTest extends KernelTestCase
{
use PrepareActivityTrait;
use PrepareCenterTrait;
use PreparePersonTrait;
use PrepareScopeTrait;
use PrepareUserTrait;
/**

View File

@ -32,6 +32,8 @@ final class AsideActivityController extends CRUDController
{
$asideActivity = new AsideActivity();
$asideActivity->setAgent($this->getUser());
$duration = $request->query->get('duration', '300');
$duration = DateTime::createFromFormat('U', $duration);
$asideActivity->setDuration($duration);

View File

@ -14,9 +14,9 @@ namespace Chill\AsideActivityBundle\Form;
use Chill\AsideActivityBundle\Entity\AsideActivity;
use Chill\AsideActivityBundle\Entity\AsideActivityCategory;
use Chill\AsideActivityBundle\Templating\Entity\CategoryRender;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use DateInterval;
use DateTime;
use DateTimeImmutable;
@ -68,22 +68,10 @@ final class AsideActivityFormType extends AbstractType
];
$builder
->add(
'agent',
EntityType::class,
[
'label' => 'For agent',
'required' => true,
'class' => User::class,
'data' => $this->storage->getToken()->getUser(),
'query_builder' => static function (EntityRepository $er) {
return $er->createQueryBuilder('u')->where('u.enabled = true');
},
'attr' => ['class' => 'select2 '],
'placeholder' => 'Choose the agent for whom this activity is created',
'choice_label' => 'username',
]
)
->add('agent', PickUserDynamicType::class, [
'label' => 'For agent',
'required' => true,
])
->add(
'date',
ChillDateType::class,

View File

@ -1,5 +1,15 @@
{% extends '@ChillMain/Admin/layout.html.twig' %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block title %}
{% include('@ChillMain/CRUD/_new_title.html.twig') %}
{% endblock %}
@ -8,4 +18,5 @@
{% embed '@ChillMain/CRUD/_new_content.html.twig' %}
{% block content_form_actions_save_and_show %}{% endblock %}
{% endembed %}
{% endblock %}
{% endblock %}

View File

@ -17,7 +17,7 @@
<select class="form-select" v-model="template">
<option disabled selected value="">{{ $t('choose_a_template') }}</option>
<template v-for="t in templates">
<option v-bind:value="t.id">{{ t.name.fr }}</option>
<option v-bind:value="t.id">{{ t.name.fr || 'Aucun nom défini' }}</option>
</template>
</select>
<button v-if="canGenerate" class="btn btn-update btn-sm change-icon" type="button" @click="generateDocument"><i class="fa fa-fw fa-cog"></i></button>
@ -48,6 +48,7 @@ export default {
required: true,
},
templates: {
type: Array,
required: true,
},
// beforeMove execute "something" before
@ -73,7 +74,11 @@ export default {
return true;
},
getDescription() {
return this.templates.find(t => t.id === this.template).description || '';
let desc = this.templates.find(t => t.id === this.template);
if (null === desc) {
return '';
}
return desc.description || '';
},
},
methods: {

View File

@ -42,10 +42,16 @@ class Address
*/
private $buildingName;
/**
* @ORM\Column(type="boolean")
* @Groups({"write"})
*/
private bool $confidential = false;
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"write"})
*/
private $corridor;
@ -78,7 +84,7 @@ class Address
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"write"})
*/
private $flat;
@ -86,7 +92,7 @@ class Address
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"write"})
*/
private $floor;
@ -143,7 +149,7 @@ class Address
/**
* @var string|null
*
* @ORM\Column(type="string", length=16, nullable=true)
* @ORM\Column(type="string", length=255, nullable=true)
* @Groups({"write"})
*/
private $steps;
@ -192,6 +198,7 @@ class Address
return (new Address())
->setAddressReference($original->getAddressReference())
->setBuildingName($original->getBuildingName())
->setConfidential($original->getConfidential())
->setCorridor($original->getCorridor())
->setCustoms($original->getCustoms())
->setDistribution($original->getDistribution())
@ -229,6 +236,11 @@ class Address
return $this->buildingName;
}
public function getConfidential(): bool
{
return $this->confidential;
}
public function getCorridor(): ?string
{
return $this->corridor;
@ -369,6 +381,13 @@ class Address
return $this;
}
public function setConfidential(bool $confidential): self
{
$this->confidential = $confidential;
return $this;
}
public function setCorridor(?string $corridor): self
{
$this->corridor = $corridor;

View File

@ -20,10 +20,9 @@ use Doctrine\ORM\Mapping as ORM;
class CommentEmbeddable
{
/**
* @var string
* @ORM\Column(type="text", nullable=true)
*/
private $comment;
private ?string $comment = null;
/**
* @var DateTime
@ -39,10 +38,7 @@ class CommentEmbeddable
*/
private $userId;
/**
* @return string
*/
public function getComment()
public function getComment(): ?string
{
return $this->comment;
}
@ -68,9 +64,6 @@ class CommentEmbeddable
return empty($this->getComment());
}
/**
* @param string $comment
*/
public function setComment(?string $comment)
{
$this->comment = $comment;

View File

@ -64,7 +64,7 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\Column(type="string", length=255, nullable=true)
* @Serializer\Groups({"read", "write"})
* @Serializer\Groups({"read", "write", "docgen:read"})
*/
private ?string $email = null;

View File

@ -0,0 +1,166 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Entity;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=ResidentialAddressRepository::class)
* @ORM\Table(name="chill_main_residential_address")
*/
class ResidentialAddress
{
/**
* @ORM\ManyToOne(targetEntity=Address::class)
* @ORM\JoinColumn(nullable=true)
*/
private ?Address $address = null;
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="residentialAddressComment_")
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="datetime_immutable", nullable=true)
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=true)
*/
private ?Person $hostPerson = null;
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class)
* @ORM\JoinColumn(nullable=true)
*/
private ?ThirdParty $hostThirdParty = null;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=false)
*/
private Person $person;
/**
* @ORM\Column(type="datetime_immutable")
*/
private ?DateTimeImmutable $startDate = null;
public function __construct()
{
$this->comment = new CommentEmbeddable();
}
public function getAddress(): ?Address
{
return $this->address;
}
public function getComment(): CommentEmbeddable
{
return $this->comment;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getHostPerson(): ?Person
{
return $this->hostPerson;
}
public function getHostThirdParty(): ?ThirdParty
{
return $this->hostThirdParty;
}
public function getId(): ?int
{
return $this->id;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function getStartDate(): ?DateTimeImmutable
{
return $this->startDate;
}
public function setAddress(?Address $address): self
{
$this->address = $address;
return $this;
}
public function setComment(CommentEmbeddable $comment): self
{
$this->comment = $comment;
return $this;
}
public function setEndDate(?DateTimeImmutable $endDate): self
{
$this->endDate = $endDate;
return $this;
}
public function setHostPerson(?Person $hostPerson): self
{
$this->hostPerson = $hostPerson;
return $this;
}
public function setHostThirdParty(?ThirdParty $hostThirdParty): self
{
$this->hostThirdParty = $hostThirdParty;
return $this;
}
public function setPerson(?Person $person): self
{
$this->person = $person;
return $this;
}
public function setStartDate(DateTimeImmutable $startDate): self
{
$this->startDate = $startDate;
return $this;
}
}

View File

@ -37,6 +37,7 @@ use function count;
class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**

View File

@ -24,6 +24,7 @@ use Doctrine\ORM\Mapping as ORM;
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**

View File

@ -12,14 +12,18 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type\DataTransformer;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
use UnexpectedValueException;
use function array_key_exists;
class UserToJsonTransformer implements DataTransformerInterface
class EntityToJsonTransformer implements DataTransformerInterface
{
private DenormalizerInterface $denormalizer;
@ -27,11 +31,14 @@ class UserToJsonTransformer implements DataTransformerInterface
private SerializerInterface $serializer;
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple)
private string $type;
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple, string $type)
{
$this->denormalizer = $denormalizer;
$this->serializer = $serializer;
$this->multiple = $multiple;
$this->type = $type;
}
public function reverseTransform($value)
@ -49,6 +56,10 @@ class UserToJsonTransformer implements DataTransformerInterface
);
}
if ('' === $value) {
return null;
}
return $this->denormalizeOne($denormalized);
}
@ -66,7 +77,7 @@ class UserToJsonTransformer implements DataTransformerInterface
]);
}
private function denormalizeOne(array $item): User
private function denormalizeOne(array $item)
{
if (!array_key_exists('type', $item)) {
throw new TransformationFailedException('the key "type" is missing on element');
@ -76,10 +87,30 @@ class UserToJsonTransformer implements DataTransformerInterface
throw new TransformationFailedException('the key "id" is missing on element');
}
switch ($this->type) {
case 'user':
$class = User::class;
break;
case 'person':
$class = Person::class;
break;
case 'thirdparty':
$class = ThirdParty::class;
break;
default:
throw new UnexpectedValueException('This type is not supported');
}
return
$this->denormalizer->denormalize(
['type' => $item['type'], 'id' => $item['id']],
User::class,
$class,
'json',
[AbstractNormalizer::GROUPS => ['read']],
);

View File

@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Form\Type\DataTransformer\UserToJsonTransformer;
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
@ -38,7 +38,7 @@ class PickUserDynamicType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new UserToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple']));
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
}
public function buildView(FormView $view, FormInterface $form, array $options)
@ -58,6 +58,6 @@ class PickUserDynamicType extends AbstractType
public function getBlockPrefix()
{
return 'pick_user_dynamic';
return 'pick_entity_dynamic';
}
}

View File

@ -0,0 +1,73 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use Chill\MainBundle\Entity\ResidentialAddress;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
final class ResidentialAddressType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('startDate', DateType::class, [
'required' => true,
'input' => 'datetime_immutable',
'widget' => 'single_text',
])
->add('endDate', DateType::class, [
'required' => false,
'input' => 'datetime_immutable',
'widget' => 'single_text',
])
->add('comment', CommentType::class, [
'required' => false,
]);
if ('person' === $options['kind']) {
$builder
->add('hostPerson', PickPersonDynamicType::class, [
'label' => 'Person',
]);
}
if ('thirdparty' === $options['kind']) {
$builder
->add('hostThirdParty', PickThirdpartyDynamicType::class, [
'label' => 'Third party',
]);
}
if ('address' === $options['kind']) {
$builder
->add('address', PickAddressType::class, [
'required' => false,
'label' => 'Address',
'use_valid_from' => false,
'use_valid_to' => false,
]);
}
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ResidentialAddress::class,
'kind' => null,
]);
}
}

View File

@ -0,0 +1,59 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Repository;
use Chill\MainBundle\Entity\ResidentialAddress;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method ResidentialAddress|null find($id, $lockMode = null, $lockVersion = null)
* @method ResidentialAddress|null findOneBy(array $criteria, array $orderBy = null)
* @method ResidentialAddress[] findAll()
* @method ResidentialAddress[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class ResidentialAddressRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, ResidentialAddress::class);
}
// /**
// * @return ResidentialAddress[] Returns an array of ResidentialAddress objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('r')
->andWhere('r.exampleField = :val')
->setParameter('val', $value)
->orderBy('r.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?ResidentialAddress
{
return $this->createQueryBuilder('r')
->andWhere('r.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View File

@ -39,6 +39,7 @@ var ShowHide = function(options) {
contents.push(el);
}
container_content.push(contents);
// console.log('container content', container_content);
}
// attach the listener on each input

View File

@ -1,21 +1,19 @@
require('./blur.scss');
var toggleBlur = function(e){
var btn = e.target;
btn.previousElementSibling.classList.toggle("blur");
btn.classList.toggle("fa-eye");
btn.classList.toggle("fa-eye-slash");
}
var infos = document.getElementsByClassName("confidential");
for(var i=0; i < infos.length; i++){
infos[i].insertAdjacentHTML('beforeend', '<i class="fa fa-eye toggle" aria-hidden="true"></i>');
}
var toggles = document.getElementsByClassName("toggle");
for(var i=0; i < toggles.length; i++){
toggles[i].addEventListener("click", toggleBlur);
}
document.querySelectorAll('.confidential').forEach(function (el) {
let i = document.createElement('i');
const classes = ['fa', 'fa-eye', 'toggle'];
i.classList.add(...classes);
el.appendChild(i);
const toggleBlur = function(e) {
for (let child of el.children) {
if (!child.classList.contains('toggle')) {
child.classList.toggle('blur');
}
}
i.classList.toggle('fa-eye');
i.classList.toggle('fa-eye-slash');
}
i.addEventListener('click', toggleBlur);
toggleBlur();
});

View File

@ -5,18 +5,21 @@ import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
const i18n = _createI18n(appMessages);
window.addEventListener('DOMContentLoaded', function(e) {
let appsOnPage = new Map();
let apps = document.querySelectorAll('[data-module="pick-dynamic"]');
function loadDynamicPicker(element) {
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
apps.forEach(function(el) {
const
isMultiple = parseInt(el.dataset.multiple) === 1,
input = document.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
picked = isMultiple ? JSON.parse(input.value) : [JSON.parse(input.value)];
uniqId = el.dataset.uniqid,
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
picked = (isMultiple) ? (JSON.parse(input.value)) : ((input.value === '[]') ? (null) : ([JSON.parse(input.value)]));
createApp({
const app = createApp({
template: '<pick-entity ' +
':multiple="multiple" ' +
':types="types" ' +
@ -65,5 +68,36 @@ window.addEventListener('DOMContentLoaded', function(e) {
})
.use(i18n)
.mount(el);
appsOnPage.set(uniqId, app);
});
});
}
document.addEventListener('show-hide-show', function(e) {
console.log('creation event caught')
loadDynamicPicker(e.detail.container)
})
document.addEventListener('show-hide-hide', function(e) {
console.log('hiding event caught')
e.detail.container.querySelectorAll('[data-module="pick-dynamic"]').forEach((el) => {
let uniqId = el.dataset.uniqid;
console.log(uniqId);
if (appsOnPage.has(uniqId)) {
appsOnPage.get(uniqId).unmount();
console.log('App has been unmounted')
appsOnPage.delete(uniqId);
}
})
})
document.addEventListener('DOMContentLoaded', function(e) {
console.log('loaded event', e)
loadDynamicPicker(document)
})

View File

@ -562,6 +562,7 @@ export default {
this.entity.loaded.cities = [];
this.entity.loaded.countries = [];
this.entity.selected.confidential = this.context.edit ? this.entity.address.confidential : false;
this.entity.selected.isNoAddress = (this.context.edit && this.entity.address.text === '') ? true : false;
this.entity.selected.country = this.context.edit ? this.entity.address.country : {};
@ -593,6 +594,7 @@ export default {
{
console.log('apply changes');
let newAddress = {
'confidential': this.entity.selected.confidential,
'isNoAddress': this.entity.selected.isNoAddress,
'street': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.street,
'streetNumber': this.entity.selected.isNoAddress ? '' : this.entity.selected.address.streetNumber,

View File

@ -6,7 +6,6 @@
<input class="form-control"
type="text"
name="floor"
maxlength=16
:placeholder="$t('floor')"
v-model="floor"/>
<label for="floor">{{ $t('floor') }}</label>
@ -15,7 +14,6 @@
<input class="form-control"
type="text"
name="corridor"
maxlength=16
:placeholder="$t('corridor')"
v-model="corridor"/>
<label for="corridor">{{ $t('corridor') }}</label>
@ -24,7 +22,6 @@
<input class="form-control"
type="text"
name="steps"
maxlength=16
:placeholder="$t('steps')"
v-model="steps"/>
<label for="steps">{{ $t('steps') }}</label>
@ -33,7 +30,6 @@
<input class="form-control"
type="text"
name="flat"
maxlength=16
:placeholder="$t('flat')"
v-model="flat"/>
<label for="flat">{{ $t('flat') }}</label>

View File

@ -17,12 +17,22 @@
<div class="row my-3">
<div class="col-lg-6">
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="isConfidential"
v-model="isConfidential"
:value="valueConfidential" />
<label class="form-check-label" for="isConfidential">
{{ $t('isConfidential') }}
</label>
</div>
<div class="form-check">
<input type="checkbox"
class="form-check-input"
id="isNoAddress"
v-model="isNoAddress"
v-bind:value="value" />
:value="value" />
<label class="form-check-label" for="isNoAddress">
{{ $t('isNoAddress') }}
</label>
@ -118,7 +128,8 @@ export default {
emits: ['getCities', 'getReferenceAddresses'],
data() {
return {
value: false
value: false,
valueConfidential: false,
}
},
computed: {
@ -134,6 +145,14 @@ export default {
addressMap() {
return this.entity.addressMap;
},
isConfidential: {
set(value) {
this.entity.selected.confidential = value;
},
get() {
return this.entity.selected.confidential;
}
},
isNoAddress: {
set(value) {
console.log('isNoAddress value', value);

View File

@ -18,6 +18,7 @@ const addressMessages = {
other_address: 'Autre adresse',
create_address: 'Adresse inconnue. Cliquez ici pour créer une nouvelle adresse',
isNoAddress: 'Pas d\'adresse complète',
isConfidential: 'Adresse confidentielle',
street: 'Nom de rue',
streetNumber: 'Numéro',
floor: 'Étage',

View File

@ -1,5 +1,5 @@
<template>
<ul class="list-suggest remove-items">
<ul class="list-suggest remove-items" v-if="picked.length">
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
<span class="chill_denomination">{{ p.text }}</span>
</li>
@ -79,11 +79,15 @@ export default {
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
console.log(this.picked)
},
removeEntity(entity) {
console.log('remove entity', entity);
this.$emit('removeEntity', entity);
}
},
mounted() {
console.log(this.picked);
}
}
</script>

View File

@ -1,22 +1,26 @@
<template>
<div class="confidential" v-on:click="toggleBlur">
<div class="confidential-content blur">
<div :class="classes">
<div class="confidential-content" :class="{ 'blur': isBlurred }">
<slot name="confidential-content"></slot>
</div>
<i class="fa fa-eye toggle" aria-hidden="true"></i>
<div>
<i class="fa fa-eye toggle" aria-hidden="true" @click="toggleBlur"></i>
</div>
</div>
</template>
<script>
export default {
name: "Confidential",
data() {
return {
isBlurred: true,
};
},
methods : {
toggleBlur: function(e){
if(e.target.matches('.toggle')){
e.target.previousElementSibling.classList.toggle("blur");
e.target.classList.toggle("fa-eye");
e.target.classList.toggle("fa-eye-slash");
}
toggleBlur() {
console.log('toggle blur');
this.isBlurred = !this.isBlurred;
},
}
}
@ -39,4 +43,4 @@ export default {
-moz-filter: blur(5px);
filter: blur(5px);
}
</style>
</style>

View File

@ -1,26 +1,58 @@
<template>
<component :is="component" class="chill-entity entity-address my-3">
<component :is="component" class="address" :class="multiline">
<div v-if="isMultiline === true">
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
{{ l }}
</p>
<div v-if="isConfidential">
<confidential>
<template v-slot:confidential-content>
<div v-if="isMultiline === true">
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
{{ l }}
</p>
</div>
<div v-else>
<p v-if="address.text"
class="street">
{{ address.text }}
</p>
<p v-if="address.postcode"
class="postcode">
{{ address.postcode.code }} {{ address.postcode.name }}
</p>
<p v-if="address.country"
class="country">
{{ address.country.name.fr }}
</p>
</div>
</template>
</confidential>
</div>
<div v-else>
<p v-if="address.text"
class="street">
{{ address.text }}
</p>
<p v-if="address.postcode"
class="postcode">
{{ address.postcode.code }} {{ address.postcode.name }}
</p>
<p v-if="address.country"
class="country">
{{ address.country.name.fr }}
</p>
<div v-if="!isConfidential">
<div v-if="isMultiline === true">
<p v-for="(l, i) in address.lines" :key="`line-${i}`">
{{ l }}
</p>
</div>
<div v-else>
<p v-if="address.text"
class="street">
{{ address.text }}
</p>
<p v-if="address.postcode"
class="postcode">
{{ address.postcode.code }} {{ address.postcode.name }}
</p>
<p v-if="address.country"
class="country">
{{ address.country.name.fr }}
</p>
</div>
</div>
</component>
<!-- <div v-if="isMultiline === true" class="address-more">
@ -78,8 +110,14 @@
</template>
<script>
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
export default {
name: 'AddressRenderBox',
components: {
Confidential
},
props: {
address: {
type: Object
@ -100,6 +138,9 @@ export default {
multiline() {
//console.log(this.isMultiline, typeof this.isMultiline);
return this.isMultiline === true ? "multiline" : "";
},
isConfidential() {
return this.address.confidential;
}
}
};

View File

@ -59,7 +59,7 @@
must be shown in such list
#}
{%- if render == 'list' -%}
<li class="chill-entity entity-address">
<li class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
{% if options['with_picto'] %}
<i class="fa fa-li fa-map-marker"></i>
{% endif %}
@ -68,7 +68,7 @@
{%- endif -%}
{%- if render == 'inline' -%}
<span class="chill-entity entity-address">
<span class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
{% if options['with_picto'] %}
<i class="fa fa-fw fa-map-marker"></i>
{% endif %}
@ -77,7 +77,7 @@
{%- endif -%}
{%- if render == 'bloc' -%}
<div class="chill-entity entity-address">
<div class="chill-entity entity-address {% if address.confidential %}confidential{% endif %}">
{% if options['has_no_address'] == true and address.isNoAddress == true %}
{% if address.postCode is not empty %}
<div class="address{% if options['multiline'] %} multiline{% endif %}{% if options['with_delimiter'] %} delimiter{% endif %}">

View File

@ -216,7 +216,7 @@
{% endif %}
{% endblock %}
{% block pick_user_dynamic_widget %}
{% block pick_entity_dynamic_widget %}
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
{% endblock %}

View File

@ -45,6 +45,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
'extra',
'validFrom' => DateTimeInterface::class,
'validTo' => DateTimeInterface::class,
'confidential',
];
private AddressRender $addressRender;
@ -83,6 +84,7 @@ class AddressNormalizer implements ContextAwareNormalizerInterface, NormalizerAw
'buildingName' => $address->getBuildingName(),
'distribution' => $address->getDistribution(),
'extra' => $address->getExtra(),
'confidential' => $address->getConfidential(),
'lines' => $this->addressRender->renderLines($address),
];

View File

@ -0,0 +1,88 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\UserRepository;
use DateTime;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use function array_key_exists;
class CommentEmbeddableDocGenNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private UserRepository $userRepository;
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @param CommentEmbeddable $object
*
* @throws ExceptionInterface
*/
public function normalize($object, ?string $format = null, array $context = []): array
{
if (null === $object) {
return [
'comment' => '',
'isNull' => true,
'date' => $this->normalizer->normalize(null, $format, array_merge($context, [
'docgen:expects' => DateTime::class,
])),
'user' => $this->normalizer->normalize(null, $format, array_merge($context, [
'docgen:expects' => User::class,
])),
];
}
$user = $this->userRepository->find($object->getUserId());
return [
'comment' => (string) $object->getComment(),
'isNull' => false,
'date' => $this->normalizer->normalize($object->getDate(), $format, array_merge($context, [
'docgen:expects' => DateTime::class,
])),
'user' => $this->normalizer->normalize($user, $format, array_merge($context, [
'docgen:expects' => User::class,
])),
];
}
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
if ('docgen' !== $format) {
return false;
}
if ($data instanceof CommentEmbeddable) {
return true;
}
if (
null === $data
&& array_key_exists('docgen:expects', $context)
&& CommentEmbeddable::class === $context['docgen:expects']) {
return true;
}
return false;
}
}

View File

@ -44,7 +44,9 @@ use function count;
final class ExportManagerTest extends KernelTestCase
{
use PrepareCenterTrait;
use PrepareScopeTrait;
use PrepareUserTrait;
private Prophet $prophet;

View File

@ -34,8 +34,11 @@ use function in_array;
final class AuthorizationHelperTest extends KernelTestCase
{
use PrepareCenterTrait;
use PrepareScopeTrait;
use PrepareUserTrait;
use ProphecyTrait;
protected function setUp(): void

View File

@ -0,0 +1,44 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Alter some address fields + add confidential field on Address.
*/
final class Version20220124085957 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address DROP confidential');
$this->addSql('ALTER TABLE chill_main_address ALTER corridor TYPE VARCHAR(16)');
$this->addSql('ALTER TABLE chill_main_address ALTER flat TYPE VARCHAR(16)');
$this->addSql('ALTER TABLE chill_main_address ALTER floor TYPE VARCHAR(16)');
$this->addSql('ALTER TABLE chill_main_address ALTER steps TYPE VARCHAR(16)');
}
public function getDescription(): string
{
return 'Alter some address fields + add confidential field on Address';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_address ADD confidential BOOLEAN DEFAULT FALSE');
$this->addSql('ALTER TABLE chill_main_address ALTER floor TYPE TEXT');
$this->addSql('ALTER TABLE chill_main_address ALTER corridor TYPE TEXT');
$this->addSql('ALTER TABLE chill_main_address ALTER steps TYPE TEXT');
$this->addSql('ALTER TABLE chill_main_address ALTER flat TYPE TEXT');
}
}

View File

@ -0,0 +1,48 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Create a new entity ResidentialAddress.
*/
final class Version20220125134253 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_main_residential_address_id_seq CASCADE');
$this->addSql('DROP TABLE chill_main_residential_address');
}
public function getDescription(): string
{
return 'Create a new entity ResidentialAddress';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_main_residential_address_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_main_residential_address (id INT NOT NULL, person_id INT NOT NULL, address_id INT DEFAULT NULL, startDate TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, endDate TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, residentialAddressComment_comment TEXT DEFAULT NULL, residentialAddressComment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, residentialAddressComment_userId INT DEFAULT NULL, hostPerson_id INT DEFAULT NULL, hostThirdParty_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_9BC1FD50217BBB47 ON chill_main_residential_address (person_id)');
$this->addSql('CREATE INDEX IDX_9BC1FD50DCA38092 ON chill_main_residential_address (hostPerson_id)');
$this->addSql('CREATE INDEX IDX_9BC1FD508DFC48DC ON chill_main_residential_address (hostThirdParty_id)');
$this->addSql('CREATE INDEX IDX_9BC1FD50F5B7AF75 ON chill_main_residential_address (address_id)');
$this->addSql('COMMENT ON COLUMN chill_main_residential_address.startDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_main_residential_address.endDate IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_main_residential_address ADD CONSTRAINT FK_9BC1FD50217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_residential_address ADD CONSTRAINT FK_9BC1FD50DCA38092 FOREIGN KEY (hostPerson_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_residential_address ADD CONSTRAINT FK_9BC1FD508DFC48DC FOREIGN KEY (hostThirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_main_residential_address ADD CONSTRAINT FK_9BC1FD50F5B7AF75 FOREIGN KEY (address_id) REFERENCES chill_main_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -52,6 +52,8 @@ Since %date%: Depuis le %date%
since %date%: depuis le %date%
Until %date%: Jusqu'au %date%
until %date%: jusqu'au %date%
Since: Depuis le
Until: Jusqu'au
#elements used in software
centers: centres
Centers: Centres

View File

@ -81,7 +81,7 @@ class AccompanyingPeriodWorkEvaluationApiController
$paginator->setItemsPerPage(count($evaluations));
return new JsonResponse($this->serializer->serialize(
new Collection($evaluations, $paginator),
new Collection(array_values($evaluations), $paginator),
'json',
[
AbstractNormalizer::GROUPS => ['read'],

View File

@ -0,0 +1,176 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Form\PersonResourceType;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\PersonResourceRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Contracts\Translation\TranslatorInterface;
final class PersonResourceController extends AbstractController
{
private EntityManagerInterface $em;
private PersonRepository $personRepository;
private PersonResourceRepository $personResourceRepository;
private TranslatorInterface $translator;
public function __construct(
PersonResourceRepository $personResourceRepository,
PersonRepository $personRepository,
EntityManagerInterface $em,
TranslatorInterface $translator
) {
$this->personResourceRepository = $personResourceRepository;
$this->personRepository = $personRepository;
$this->em = $em;
$this->translator = $translator;
}
public function deleteAction(Request $request, $person_id, $resource_id): Response
{
$personOwner = $this->personRepository->find($person_id);
$resource = $this->personResourceRepository->find($resource_id);
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $personOwner);
if (null === $resource) {
throw $this->createNotFoundException('Unable to find Resource entity.');
}
$form = $this->createFormBuilder()
->setAction($this->generateUrl('chill_person_resource_delete', [
'resource_id' => $resource_id,
'person_id' => $person_id,
]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
if ($request->getMethod() === Request::METHOD_DELETE) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->em->remove($resource);
$this->em->flush();
$this->addFlash('success', $this->translator
->trans('The resource has been successfully removed.'));
return $this->redirectToRoute('chill_person_resource_list', [
'person_id' => $personOwner->getId(),
]);
}
}
return $this->render(
'ChillPersonBundle:PersonResource:delete.html.twig',
[
'person' => $personOwner,
'resource' => $resource,
'form' => $form->createView(),
]
);
}
public function editAction(Request $request, $resource_id, $person_id): Response
{
$resource = $this->personResourceRepository->find($resource_id);
$personOwner = $this->personRepository->find($person_id);
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $personOwner);
if (null === $resource) {
throw $this->createNotFoundException('Unable to find Resource entity.');
}
$form = $this->createForm(PersonResourceType::class, $resource);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->em->persist($resource);
$this->em->flush();
return $this->redirectToRoute('chill_person_resource_list', [
'person_id' => $personOwner->getId(),
]);
}
return $this->render(
'ChillPersonBundle:PersonResource:edit.html.twig',
[
'person' => $personOwner,
'resource' => $resource,
'form' => $form->createView(),
'action' => 'edit',
]
);
}
public function listAction(Request $request, $person_id)
{
$personOwner = $this->personRepository->find($person_id);
$this->denyAccessUnlessGranted(PersonVoter::SEE, $personOwner);
$personResources = [];
$personResources = $this->personResourceRepository->findBy(['personOwner' => $personOwner->getId()]);
$form = $this->createForm(PersonResourceType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->denyAccessUnlessGranted(PersonVoter::CREATE, $personOwner);
$personResource = new PersonResource();
$person = $form['person']->getData();
$thirdparty = $form['thirdparty']->getData();
$freetext = $form['freetext']->getData();
$comment = $form['comment']->getData();
$kind = $form['kind']->getData();
$personResource->setKind($kind);
$personResource->setPerson($person);
$personResource->setThirdParty($thirdparty);
$personResource->setFreeText($freetext);
$personResource->setComment($comment);
$personResource->setPersonOwner($personOwner);
$this->em->persist($personResource);
$this->em->flush();
return $this->redirectToRoute('chill_person_resource_list', [
'person_id' => $personOwner->getId(),
]);
}
return $this->render(
'ChillPersonBundle:PersonResource:list.html.twig',
[
'person' => $personOwner,
'personResources' => $personResources,
'form' => $form->createView(),
]
);
}
}

View File

@ -0,0 +1,161 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\ResidentialAddress;
use Chill\MainBundle\Form\Type\ResidentialAddressType;
use Chill\MainBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class ResidentialAddressController extends AbstractController
{
private UrlGeneratorInterface $generator;
private ResidentialAddressRepository $residentialAddressRepository;
private TranslatorInterface $translator;
public function __construct(
UrlGeneratorInterface $generator,
TranslatorInterface $translator,
ResidentialAddressRepository $residentialAddressRepository
) {
$this->generator = $generator;
$this->translator = $translator;
$this->residentialAddressRepository = $residentialAddressRepository;
}
/**
* @Route("/{_locale}/person/residential-address/{id}/delete", name="chill_person_residential_address_delete")
*/
public function deleteAction(Request $request, ResidentialAddress $residentialAddress): Response
{
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $residentialAddress->getPerson());
$form = $this->createForm(FormType::class);
$form->add('submit', SubmitType::class, ['label' => 'Delete']);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$em = $this->getDoctrine()->getManager();
$em->remove($residentialAddress);
$em->flush();
$this->addFlash('success', $this->translator->trans('Residential address had been deleted'));
return $this->redirectToRoute('chill_person_residential_address_list', ['id' => $residentialAddress->getPerson()->getId()]);
}
return $this->render('@ChillPerson/ResidentialAddress/delete.html.twig', [
'person' => $residentialAddress->getPerson(),
'residentialAddress' => $residentialAddress,
'delete_form' => $form->createView(),
]);
}
/**
* @Route("/{_locale}/person/residential-address/{id}/edit", name="chill_person_residential_address_edit")
*/
public function editAction(Request $request, ResidentialAddress $residentialAddress): Response
{
if ($request->query->has('kind')) {
$kind = $request->query->getAlpha('kind', '');
} else {
$kind = null;
}
$person = $residentialAddress->getPerson();
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person);
$form = $this->createForm(ResidentialAddressType::class, $residentialAddress, ['kind' => $kind]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', $this->translator
->trans('The residential address was updated successfully'));
return $this->redirect(
$request->get('returnPath', null) ??
$this->generator->generate('chill_person_residential_address_list', ['id' => $person->getId()])
);
}
return $this->render('@ChillPerson/ResidentialAddress/edit.html.twig', [
'residentialAddress' => $residentialAddress,
'person' => $person,
'form' => $form->createView(),
]);
}
/**
* @Route("/{_locale}/person/{id}/residential-address/list", name="chill_person_residential_address_list")
*/
public function listAction(Request $request, Person $person): Response
{
$this->denyAccessUnlessGranted(PersonVoter::SEE, $person);
$residentialAddresses = $this->residentialAddressRepository->findBy(['person' => $person], ['startDate' => 'DESC']);
return $this->render('@ChillPerson/ResidentialAddress/list.html.twig', [
'person' => $person,
'addresses' => $residentialAddresses,
]);
}
/**
* @Route("/{_locale}/person/{id}/residential-address/new", name="chill_person_residential_address_new")
*/
public function newAction(Request $request, Person $person): Response
{
$residentialAddress = new ResidentialAddress();
$residentialAddress->setPerson($person);
$this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person);
if (!$request->query->has('kind')) {
return $this->render('@ChillPerson/ResidentialAddress/new_pick_kind.html.twig', ['person' => $person]);
}
$kind = $request->query->getAlpha('kind', '');
$form = $this->createForm(ResidentialAddressType::class, $residentialAddress, ['kind' => $kind]);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->getDoctrine()->getManager()->persist($residentialAddress);
$this->getDoctrine()->getManager()->flush();
$this->addFlash('success', $this->translator
->trans('The new residential address was created successfully'));
return $this->redirect(
$request->get('returnPath', null) ??
$this->generator->generate('chill_person_residential_address_list', ['id' => $residentialAddress->getPerson()->getId()])
);
}
return $this->render('@ChillPerson/ResidentialAddress/new.html.twig', [
'person' => $person,
'form' => $form->createView(),
]);
}
}

View File

@ -33,6 +33,7 @@ use Symfony\Component\Validator\Constraints as Assert;
class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**

View File

@ -784,12 +784,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* If the `$at` parameter is now, use the method `getCurrentPersonAddress`, which is optimized
* on database side.
*
* @deprecated since chill2.0, address is linked to the household. Use @see{Person::getCurrentHouseholdAddress}
*
* @throws Exception
*/
public function getAddressAt(?DateTime $at = null): ?Address
public function getAddressAt(?DateTimeInterface $at = null): ?Address
{
$at ??= new DateTime('now');
if ($at instanceof DateTimeImmutable) {
$at = DateTime::createFromImmutable($at);
}
/** @var ArrayIterator $addressesIterator */
$addressesIterator = $this->getAddresses()
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
@ -950,6 +956,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
: null;
}
/**
* Get the household address at the given date.
*
* if the given date is 'now', use instead @see{getCurrentPersonAddress}, which is optimized on
* database side.
*/
public function getCurrentHouseholdAddress(?DateTimeImmutable $at = null): ?Address
{
if (

View File

@ -0,0 +1,209 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Person;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_resource")
* @DiscriminatorMap(typeProperty="type", mapping={
* "personResource": personResource::class
* })
*/
class PersonResource implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
* @Groups({"read"})
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="text", nullable=true)
* @Groups({"read"})
*/
private ?string $freeText = null;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity=PersonResourceKind::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
*/
private $kind;
/**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
*/
private ?Person $person = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/
private ?Person $personOwner = null;
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
*/
private ?ThirdParty $thirdParty = null;
public function __construct()
{
$this->comment = new CommentEmbeddable();
}
public function getComment(): CommentEmbeddable
{
return $this->comment;
}
public function getFreeText(): ?string
{
return $this->freeText;
}
/**
* GETTERS.
*/
public function getId(): ?int
{
return $this->id;
}
public function getKind(): ?PersonResourceKind
{
return $this->kind;
}
public function getPerson(): ?Person
{
return $this->person;
}
public function getPersonOwner(): ?Person
{
return $this->personOwner;
}
public function getThirdParty(): ?ThirdParty
{
return $this->thirdParty;
}
public function setComment(?CommentEmbeddable $comment): self
{
if (null === $comment) {
$this->comment->setComment('');
return $this;
}
$this->comment = $comment;
return $this;
}
public function setFreeText(?string $freeText): self
{
$this->freeText = $freeText;
if ('' !== $freeText && null !== $freeText) {
$this->setPerson(null);
$this->setThirdParty(null);
}
if ('' === $freeText) {
$this->freeText = null;
}
return $this;
}
public function setKind(?PersonResourceKind $kind): self
{
$this->kind = $kind;
return $this;
}
public function setPerson(?Person $person): self
{
$this->person = $person;
if (null !== $person) {
$this->setFreeText('');
$this->setThirdParty(null);
}
return $this;
}
public function setPersonOwner(?Person $personOwner): self
{
$this->personOwner = $personOwner;
return $this;
}
public function setThirdParty(?ThirdParty $thirdParty): self
{
$this->thirdParty = $thirdParty;
if (null !== $thirdParty) {
$this->setFreeText('');
$this->setPerson(null);
}
return $this;
}
/**
* @Assert\Callback
*
* @param mixed $payload
*/
public function validate(ExecutionContextInterface $context, $payload)
{
if (null === $this->person && null === $this->thirdParty && (null === $this->freeText || '' === $this->freeText)) {
$context->buildViolation('You must associate at least one entity')
->addViolation();
}
}
}

View File

@ -0,0 +1,69 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
/**
* **About denormalization**: this operation is operated by @see{AccompanyingPeriodResourdeNormalizer}.
*
* @ORM\Entity
* @ORM\Table(name="chill_person_resource_kind")
*/
class PersonResourceKind
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private int $id;
/**
* @ORM\Column(type="boolean")
*/
private bool $isActive = true;
/**
* @ORM\Column(type="json", length=255)
*/
private array $title;
public function getId(): ?int
{
return $this->id;
}
public function getIsActive(): bool
{
return $this->isActive;
}
public function getTitle(): ?array
{
return $this->title;
}
public function setIsActive(bool $isActive): self
{
$this->isActive = $isActive;
return $this;
}
public function setTitle(array $title): self
{
$this->title = $title;
return $this;
}
}

View File

@ -0,0 +1,95 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Entity\Person\PersonResourceKind;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\ResourceKindRender;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Contracts\Translation\TranslatorInterface;
final class PersonResourceType extends AbstractType
{
private PersonRender $personRender;
private ResourceKindRender $resourceKindRender;
private ThirdPartyRender $thirdPartyRender;
private TranslatorInterface $translator;
public function __construct(ResourceKindRender $resourceKindRender, PersonRender $personRender, ThirdPartyRender $thirdPartyRender, TranslatorInterface $translator)
{
$this->resourceKindRender = $resourceKindRender;
$this->personRender = $personRender;
$this->thirdPartyRender = $thirdPartyRender;
$this->translator = $translator;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('kind', EntityType::class, [
'label' => 'Type',
'required' => false,
'class' => PersonResourceKind::class,
'query_builder' => static function (EntityRepository $er) {
$qb = $er->createQueryBuilder('pr');
$qb->where($qb->expr()->eq('pr.isActive', 'TRUE'));
return $qb;
},
'placeholder' => $this->translator->trans('Select a type'),
'choice_label' => function (PersonResourceKind $personResourceKind) {
$options = [];
return $this->resourceKindRender->renderString($personResourceKind, $options);
},
])
->add('person', PickPersonDynamicType::class, [
'label' => 'Usager',
])
->add('thirdparty', PickThirdpartyDynamicType::class, [
'label' => 'Tiers',
])
->add('freetext', ChillTextareaType::class, [
'label' => 'Description libre',
'required' => false,
])
->add('comment', CommentType::class, [
'label' => 'Note',
'required' => false,
]);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => PersonResource::class,
]);
}
public function getBlockPrefix(): string
{
return 'chill_personbundle_person_resource';
}
}

View File

@ -0,0 +1,62 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Form\Type;
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Pick user dymically, using vuejs module "AddPerson".
*/
class PickPersonDynamicType extends AbstractType
{
private DenormalizerInterface $denormalizer;
private SerializerInterface $serializer;
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer)
{
$this->denormalizer = $denormalizer;
$this->serializer = $serializer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'person'));
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['person'];
$view->vars['uniqid'] = uniqid('pick_user_dyn');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
}
public function getBlockPrefix()
{
return 'pick_entity_dynamic';
}
}

View File

@ -62,6 +62,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
'order' => 50,
]);
$menu->addChild($this->translator->trans('Residential addresses'), [
'route' => 'chill_person_residential_address_list',
'routeParameters' => [
'id' => $parameters['person']->getId(),
],
])
->setExtras([
'order' => 60,
]);
$menu->addChild($this->translator->trans('household.person history'), [
'route' => 'chill_person_household_person_history',
'routeParameters' => [
@ -96,6 +106,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
'order' => 100,
]);
}
$menu->addChild($this->translator->trans('person_resources_menu'), [
'route' => 'chill_person_resource_list',
'routeParameters' => [
'person_id' => $parameters['person']->getId(),
],
])
->setExtras([
'order' => 99999,
]);
}
public static function getMenuIds(): array

View File

@ -0,0 +1,61 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
final class PersonResourceRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(PersonResource::class);
}
public function find($id): ?PersonResource
{
return $this->repository->find($id);
}
/**
* @return PersonResource[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param mixed|null $limit
* @param mixed|null $offset
*
* @return PersonResource[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findOneBy(array $criteria): ?PersonResource
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return PersonResource::class;
}
}

View File

@ -0,0 +1,67 @@
import {createApp} from 'vue';
import SetReferrer from 'ChillPersonAssets/vuejs/_components/AccompanyingPeriod/SetReferrer.vue';
import {fetchResults} from 'ChillMainAssets/lib/api/apiMethods.js';
/**
*
* To start this app, add this container into recordAction passed as argument to
* `ChillPerson/AccompanyingPeriod/_list_item.html.twig`:
*
* ```html+twig
* {% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE', period) %}
* <li>
* <span data-set-referrer-app="data-set-referrer-app" data-set-referrer-accompanying-period-id="{{ period.id }}"></span>
* </li>
* {% endif %}
* ```
*
* The app will update the referrer displayed into dedicated span
*/
document.querySelectorAll('[data-set-referrer-app]').forEach(function (el) {
let
periodId = Number.parseInt(el.dataset.setReferrerAccompanyingPeriodId);
const url = `/api/1.0/person/accompanying-course/${periodId}/referrers-suggested.json`;
fetchResults(url).then(suggested => {
const app = createApp({
components: {
SetReferrer,
},
template:
'<set-referrer :suggested="suggested" :periodId="periodId" @referrerSet="onReferrerSet"></set-referrer>',
data() {
return {
periodId, suggested, original: suggested,
}
},
methods: {
onReferrerSet(ref) {
const bloc = document.querySelector(`[data-accompanying-period-id="${this.periodId}"]`);
if (bloc === null) {
console.error('bloc not found');
return;
}
const label = bloc.querySelector('[data-referrer-text]');
if (label === null) {
console.error('label not found');
return;
}
label.textContent = ref.text;
label.classList.remove('chill-no-data-statement');
this.suggested = this.original.filter(user => user.id !== ref.id);
}
}
});
app.mount(el);
})
})

View File

@ -0,0 +1,55 @@
import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js';
window.addEventListener('DOMContentLoaded', function() {
let
personContainer = document.querySelector('#person-entity'),
entitySelector = document.querySelector('#entity-selector'),
freetextContainer = document.querySelector('#freetext-entity'),
thirdpartyContainer = document.querySelector('#thirdparty-entity')
;
if (null === entitySelector) {
return;
}
new ShowHide({
debug: false,
load_event: null,
froms: [entitySelector],
container: [personContainer],
test: function(froms, event) {
for (let container of froms) {
return container.querySelector('input[value="person"]').checked;
}
console.log('we couldnt find the input');
return false;
},
})
new ShowHide({
debug: false,
load_event: null,
froms: [entitySelector],
container: [thirdpartyContainer],
test: function(froms, event) {
for (let container of froms) {
return container.querySelector('input[value="thirdparty"]').checked;
}
console.log('we couldnt find the input');
return false;
},
})
new ShowHide({
debug: false,
load_event: null,
froms: [entitySelector],
container: [freetextContainer],
test: function(froms, event) {
for (let container of froms) {
return container.querySelector('input[value="freetext"]').checked;
}
console.log('we couldnt find the input');
return false;
},
})
});

View File

@ -58,7 +58,11 @@ const store = createStore({
return state.thirdParties.length > 0;
},
getTemplatesAvailablesForEvaluation: (state) => (evaluation) => {
return state.templatesAvailablesForEvaluation.get(evaluation.id) || [];
if (state.templatesAvailablesForEvaluation.has(evaluation.id)) {
return state.templatesAvailablesForEvaluation.get(evaluation.id);
}
return [];
},
buildPayload(state) {
return {
@ -171,7 +175,6 @@ const store = createStore({
};
g.id = tmpIndex() -1
state.goalsPicked.push(g);
//console.log('goals picked ids', state.goalsPicked.map(g => g.id))
},
removeGoal(state, goal) {
state.goalsPicked = state.goalsPicked.filter(g => g.id !== goal.id);
@ -369,7 +372,6 @@ const store = createStore({
return makeFetch('PUT', url, payload)
.then(data => {
console.log('data received', data);
if (typeof(callback) !== 'undefined') {
return callback(data);
} else {

View File

@ -0,0 +1,6 @@
export const lightGreen = '#43b29d';
export const darkGreen = '#368e7e';
export const lightBrown = '#a2ac80';
export const darkBrown = '#929d69';
export const lightBlue = '#8d9dab';
export const darkBlue = '#718596';

View File

@ -2,6 +2,7 @@ import { createStore } from 'vuex'
import { getHouseholdByPerson, getCoursesByPerson, getRelationshipsByPerson } from './api'
import { getHouseholdLabel, getHouseholdWidth, getRelationshipLabel, getRelationshipTitle, getRelationshipDirection, splitId, getGender, getAge } from './vis-network'
import {visMessages} from "./i18n";
import { darkBlue, darkBrown, darkGreen, lightBlue, lightBrown, lightGreen } from './colors';
const debug = process.env.NODE_ENV !== 'production'
@ -172,8 +173,8 @@ const store = createStore({
id: 'relationship_' + splitId(link.id,'id')
+ '-person_' + link.fromPerson.id + '-person_' + link.toPerson.id,
arrows: getRelationshipDirection(link),
color: 'lightblue',
font: { color: '#33839d' },
color: lightGreen,
font: { color: darkGreen },
dashes: true,
label: getRelationshipLabel(link),
title: getRelationshipTitle(link),
@ -316,10 +317,10 @@ const store = createStore({
to: `${household.id}`,
id: `${household.id}-person_${m.person.id}`,
arrows: 'from',
color: 'pink',
font: { color: '#D04A60' },
color: lightBrown,
font: { color: darkBrown },
dashes: (getHouseholdWidth(m) === 1)? [0,4] : false, //edge style: [dash, gap, dash, gap]
label: getHouseholdLabel(m),
//label: getHouseholdLabel(m),
width: getHouseholdWidth(m),
})
if (!getters.isPersonLoaded(m.person.id)) {
@ -375,8 +376,8 @@ const store = createStore({
to: `${course.id}`,
id: `accompanying_period_${splitId(course.id,'id')}-person_${p.person.id}`,
arrows: 'from',
color: 'orange',
font: { color: 'darkorange' },
color: lightBlue,
font: { color: darkBlue },
})
if (!getters.isPersonLoaded(p.person.id)) {
dispatch('addMissingPerson', [p.person, course])
@ -428,8 +429,8 @@ const store = createStore({
id: 'relationship_' + splitId(relationship.id,'id')
+ '-person_' + relationship.fromPerson.id + '-person_' + relationship.toPerson.id,
arrows: getRelationshipDirection(relationship),
color: 'lightblue',
font: { color: '#33839d' },
color: lightGreen,
font: { color: darkGreen },
dashes: true,
label: getRelationshipLabel(relationship),
title: getRelationshipTitle(relationship),

View File

@ -1,4 +1,5 @@
import { visMessages } from './i18n'
import { darkGreen, lightBlue, lightBrown, lightGreen } from './colors';
import { visMessages } from './i18n';
/**
* Vis-network initial data/configuration script
@ -15,12 +16,12 @@ window.options = {
/*
*/
configure: {
enabled: true,
enabled: false,
filter: 'physics',
showButton: true
},
physics: {
enabled: true,
enabled: false,
barnesHut: {
theta: 0.5,
gravitationalConstant: -2000,
@ -89,7 +90,7 @@ window.options = {
edges: {
font: {
color: '#b0b0b0',
size: 9,
size: 14,
face: 'arial',
background: 'none',
strokeWidth: 2, // px
@ -112,30 +113,30 @@ window.options = {
},
color: {
border: '#b0b0b0',
background: 'rgb(193,229,222)',
background: lightGreen,
highlight: {
border: '#89c9a9',
background: 'rgb(156,213,203)'
border: '#216458',
background: darkGreen,
},
hover: {
border: '#89c9a9',
background: 'rgb(156,213,203)'
border: '#216458',
background: darkGreen,
}
},
opacity: 0.85,
opacity: 0.9,
shadow:{
enabled: true,
color: 'rgba(0,0,0,0.5)',
size:10,
x:5,
y:5
y:5,
},
},
household: {
color: 'pink'
color: lightBrown,
},
accompanying_period: {
color: 'orange',
color: lightBlue,
},
}
}

View File

@ -0,0 +1,51 @@
<template>
<ul class="list-suggest add-items" v-if="suggested.length > 0">
<li v-for="r in suggested" @click="setReferrer(r)"><span>{{ r.text }}</span></li>
</ul>
</template>
<script>
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
export default {
name: "SetReferrer",
props: {
suggested: {
type: Array,
required: false,
//default: [],
},
periodId: {
type: Number,
required: true
}
},
data() {
return {
/*suggested: [
{id: 5, text: 'Robert'}, {id: 8, text: 'Monique'},
]*/
}
},
emits: ['referrerSet'],
methods: {
setReferrer: function(ref) {
const url = `/api/1.0/person/accompanying-course/${this.periodId}.json`;
const body = { type: "accompanying_period", user: { id: ref.id, type: ref.type }};
return makeFetch('PATCH', url, body)
.then((response) => {
this.$emit('referrerSet', ref);
})
.catch((error) => {
throw error;
})
}
}
}
</script>
<style scoped>
</style>

View File

@ -77,7 +77,10 @@
<h4 class="item-key visually-hidden">{{ 'Pinned comment'|trans }}</h4>
<blockquote class="chill-user-quote">
<i class="fa fa-flag float-end text-chill-gray" title="{{ 'pinned'|trans }}"></i>
{{ accompanyingCourse.pinnedComment.content|chill_markdown_to_html }}
{{ accompanyingCourse.pinnedComment.content|u.truncate(250, '…', false)|chill_markdown_to_html }}
{% if accompanyingCourse.pinnedComment.content|length > 250 %}
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_comment_list', {'accompanying_period_id': accompanyingCourse.id}) }}">{{ 'Read more'|trans }}</a>
{% endif %}
<div class="metadata">
{{ 'Last updated by'| trans }}
<span class="user">
@ -148,14 +151,14 @@
{% if accompanyingCourse.requestorPerson is not null %}
<h4 class="item-key">{{ 'Requestor'|trans }}</h4>
{% if accompanyingCourse.requestorAnonymous %}
<div class="confidential"><p class="blur">{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}</p></div>
<div class="confidential"><p>{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}</p></div>
{% else %}
{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}
{% endif %}
{% elseif accompanyingCourse.requestorThirdParty is not null %}
<h4 class="item-key">{{ 'Requestor'|trans }}</h4>
{% if accompanyingCourse.requestorAnonymous %}
<div class="confidential"><p class="blur">{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}</p></div>
<div class="confidential"><p>{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}</p></div>
{% else %}
{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}
{% endif %}

View File

@ -1,4 +1,4 @@
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass|raw }}{% endif %}" data-accompanying-period-id="{{ period.id|e('html_attr') }}">
<div class="item-row">
<div class="wrap-header">
<div class="wh-row">
@ -43,11 +43,12 @@
</div>
<div class="wh-col">
{% if chill_accompanying_periods.fields.user == 'visible' %}
{# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #}
{% if period.user %}
<abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr>
{{ period.user|chill_entity_render_box }}
<span data-referrer-text="data-referrer-text">{{ period.user|chill_entity_render_box }}</span>
{% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
<span class="chill-no-data-statement" data-referrer-text="data-referrer-text">{{ 'No accompanying user'|trans }}</span>
{% endif %}
{% endif %}
</div>

View File

@ -0,0 +1,15 @@
<div class="col-md col-xxl">
<div id="collapseForm" class="{% if not form.vars.submitted %}collapse{% endif %}">
<h3 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h3>
{% include "@ChillPerson/PersonResource/form.html.twig" %}
</div>
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-primary btn-create change-icon" type="button" data-bs-toggle="collapse" data-bs-target="#collapseForm" aria-expanded="false" aria-controls="collapseForm">
{{ 'Add a person resource'|trans }}
</button>
</li>
</ul>
</div>

View File

@ -0,0 +1,17 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_person_resource_list' %}
{# {% set person = person %} #}
{% block title 'Remove resource'|trans %}
{% block personcontent %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Remove resource'|trans,
'confirm_question' : 'Are you sure you want to remove the resource for "%name%" ?'|trans({ '%name%' : person.firstname ~ ' ' ~ person.lastname } ),
'cancel_route' : 'chill_person_resource_list',
'cancel_parameters' : { 'person_id' : person.id },
'form' : form
} ) }}
{% endblock %}

View File

@ -0,0 +1,23 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_person_resource_edit' %}
{% block js %}
{{ encore_entry_script_tags('page_person_resource_showhide_input') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('page_person_resource_showhide_input') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block title %}{{ 'edit resource'|trans|capitalize }}{% endblock %}
{% block personcontent %}
<h1 style="margin-bottom: 2rem;">{{ 'edit resource'|trans }}</h1>
{% include "@ChillPerson/PersonResource/form.html.twig" %}
{% endblock %}

View File

@ -0,0 +1,77 @@
{{ form_start(form, {'attr' : {'id' : 'create-form'}}) }}
{{ form_errors(form) }}
{{ form_row(form.kind) }}
<div id="linked-entity">
<fieldset class="mb-3">
<div class="row">
<legend class="col-sm-4 col-form-label">Associer un</legend>
<div class="col-sm-8">
<div id="entity-selector">
<div class="form-check">
{% if resource is defined and resource.person is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_0" name="linked-entity" class="form-check-input" value="person" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'person' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">Usager</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_0" name="linked-entity" class="form-check-input" value="person" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_0">Usager</label>
{% endif %}
</div>
<div class="form-check">
{% if resource is defined and resource.thirdparty is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_1" name="linked-entity" class="form-check-input" value="thirdparty" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'thirdparty' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">Tiers</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_1" name="linked-entity" class="form-check-input" value="thirdparty" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_1">Tiers</label>
{% endif %}
</div>
<div class="form-check">
{% if resource is defined and resource.freeText is not null %}
<input checked type="radio" id="chill_personbundle_person_resource_linkedEntity_2" name="linked-entity" class="form-check-input" value="freetext" {% if form.vars.submitted and app.request.request.get('linked-entity', null) == 'thirdparty' %}checked{% endif %}/>
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">Description libre</label>
{% else %}
<input type="radio" id="chill_personbundle_person_resource_linkedEntity_2" name="linked-entity" class="form-check-input" value="freetext" />
<label class="form-check-label" for="chill_personbundle_person_resource_linkedEntity_2">Description libre</label>
{% endif %}
</div>
</div>
</div>
</div>
</fieldset>
</div>
<div id="person-entity">
{{ form_row(form.person) }}
</div>
<div id="thirdparty-entity">
{{ form_row(form.thirdparty) }}
</div>
<div id="freetext-entity">
{{ form_row(form.freetext) }}
</div>
{{ form_row(form.comment) }}
{% if action is defined %}
<ul class="record_actions sticky-form-buttons">
<li class="edit">
<button class="btn btn-edit"
type="submit" id="newPersonResource">
{{ 'edit resource'|trans|capitalize }}
</button>
</li>
</ul>
{% else %}
<ul class="record_actions">
<li>
<button class="btn btn-create"
type="submit" id="newPersonResource">
{{ 'Save'|trans }}
</button>
</li>
</ul>
{% endif %}
{{ form_end(form) }}

View File

@ -0,0 +1,89 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_person_resource_list' %}
{% block title %}{{ 'Person resources'|trans|capitalize ~ ' ' ~ person|chill_entity_render_string }}{% endblock %}
{% block js %}
{{ encore_entry_script_tags('page_person_resource_showhide_input') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('page_person_resource_showhide_input') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block personcontent %}
<h1>{{ 'List of resources'|trans }}</h1>
{% if personResources|length > 0 %}
<div class="flex-table">
{% for resource in personResources %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
{% if resource.person is not null %}
<div class="denomination h3">
<span class="name">{{ resource.person }}</span>
<span class="badge rounded-pill bg-person">{{ 'person'|trans|capitalize }}</span>
</div>
{% elseif resource.thirdparty is not null %}
<div class="denomination h3">
<span class="name">{{ resource.thirdparty }}</span>
<span class="badge rounded-pill bg-thirdparty">
{{ 'thirdparty'|trans|capitalize }}
<i class="fa fa-fw fa-user-md"></i>
</span>
</div>
{% else %}
<div class="denomination h3">
<span>{{ resource.freetext }}</span>
</div>
{% endif %}
</div>
<div class="item-col">
{% if resource.kind %}
<span>{{ resource.kind.title.fr|capitalize }}</span>
{% endif %}
</div>
</div>
{% if resource.comment.comment is not empty %}
<div class="item-row separator">
<section class="chill-entity entity-comment-embeddable">
<div>{{ resource.comment|chill_entity_render_box }}<div>
</section>
</div>
{% endif %}
{% if is_granted('CHILL_PERSON_UPDATE', resource.person) %}
<div class="item-row separator">
<div class="item-col">
<ul class="record_actions">
<li>
<a href="{{ chill_path_add_return_path('chill_person_resource_edit', {'resource_id': resource.id,
'person_id': person.id,}) }}"
class="btn btn-sm btn-edit"
title="{{ 'Edit'|trans }}"></a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_resource_delete', {'person_id': person.id,
'resource_id': resource.id}) }}"
class="btn btn-sm btn-delete"
title="{{ 'Delete'|trans }}"></a>
</li>
</ul>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
{% else %}
<p class="chill-no-data-statement">{{ 'There are no available resources'|trans }}</p>
{% endif %}
{% include "@ChillPerson/PersonResource/create.html.twig" %}
{% endblock %}

View File

@ -0,0 +1,37 @@
{{ form_start(form) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
{% if form.hostPerson is defined %}
{{ form_row(form.hostPerson) }}
{% endif %}
{% if form.hostThirdParty is defined %}
{{ form_row(form.hostThirdParty) }}
{% endif %}
{% if form.address is defined %}
{{ form_row(form.address) }}
{% block js %}
{{ encore_entry_script_tags('mod_input_address') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_input_address') }}
{% endblock %}
{% endif %}
{{ form_row(form.comment) }}
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-update" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}

View File

@ -0,0 +1,58 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title %}{{ 'Delete residential address'|trans }}{% endblock %}
{% block personcontent %}
<div class="address-new">
<h1>{{ block('title') }}</h1>
{% set a = residentialAddress %}
<ul class="list-content fa-ul">
{% if a.hostPerson is not null %}
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}} </span>
<span class="chill-entity entity-person badge-person">{{ a.hostPerson|chill_entity_render_box }}</span>
</li>
<li>
{% set address_date = date(a.startDate|date("m/d/Y")) %}
{% if a.hostPerson.getCurrentHouseholdAddress(a.endDate) is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.hostPerson.getCurrentHouseholdAddress(a.endDate)|chill_entity_render_box }}
{% endif %}
</li>
{% elseif a.hostThirdParty is not null %}
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}}</span>
<span class="chill-entity entity-person badge-person">{{ a.hostThirdParty|chill_entity_render_box }}</span>
</li>
<li>
{% if a.hostThirdParty.address is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.hostThirdParty.address|chill_entity_render_box }}
{% endif %}
</li>
{% else %}
<li>
{% if a.address is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.address|chill_entity_render_box }}
{% endif %}
{% endif %}
</li>
</ul>
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Delete residential address ?'|trans,
'confirm_question' : 'Are you sure you want to remove this residential address for %name% ?'|trans({'%name%': person|chill_entity_render_string }),
'cancel_route' : 'chill_person_residential_address_list',
'cancel_parameters' : {'id' : person.Id},
'form' : delete_form
} ) }}
</div>
{% endblock %}

View File

@ -0,0 +1,51 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title 'Edit a residential address'|trans %}
{% block personcontent %}
<div class="address-edit">
{% block content %}
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
{{ form_row(form.startDate) }}
{{ form_row(form.endDate) }}
{% if residentialAddress.address is not null %}
{% if form.address is defined %}
{{ form_row(form.address) }}
{% block js %}
{{ encore_entry_script_tags('mod_input_address') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_input_address') }}
{% endblock %}
{% endif %}
{% endif %}
{{ form_row(form.comment) }}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_path_add_return_path('chill_person_residential_address_list', {'id': person.id}) }}" class="btn btn-cancel">
{{ 'Cancel'|trans|chill_return_path_label }}
</a>
</li>
<li>
<button class="btn btn-update" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}
</div>
{% endblock %}

View File

@ -0,0 +1,116 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title %}{{ 'Residential addresses history for %name%'|trans({ '%name%': person.firstName ~ ' ' ~ person.lastName } ) }}{% endblock %}
{% block personcontent %}
<div class="person-address">
<h1>{{ 'Residential addresses history'|trans }}</h1>
{% if is_granted('CHILL_PERSON_SEE', person) %}
{% if addresses|length == 0 %}
<span class="chill-no-data-statement">{{ 'No address given'|trans }}</span>
{% else %}
<div class="flex-table">
{% for a in addresses %}
{% if a.address is not null %}
{% set kind = 'address' %}
{% else %}
{% set kind = null %}
{% endif %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col" style="width: 33%;">
<ul class="list-unstyled h3">
{% if a.endDate is not null %}
<li><span class="item-key">{{'Since'|trans}} : </span>{{ a.startDate|format_date('long') }}</li>
{% endif %}
<li><span class="item-key">{{'Until'|trans}} : </span><b>{{ a.endDate|format_date('long') }}</b></li>
</ul>
</div>
<div class="item-col flex-column justify-content-start">
<div class="float-button top">
<div class="box">
<div class="action">
{% if is_granted('CHILL_PERSON_UPDATE', person) %}
<a href="{{ path('chill_person_residential_address_edit', { 'id' : a.id, 'kind' : kind } ) }}" class="btn btn-update" title="{{ 'Update'|trans|e('html_attr') }}"></a>
<a href="{{ path('chill_person_residential_address_delete', { 'id' : a.id, 'kind' : kind } ) }}" class="btn btn-delete" title="{{ 'Delete'|trans|e('html_attr') }}"></a>
{% endif %}
</div>
<ul class="list-content fa-ul">
{% if a.hostPerson is not null %}
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}} </span>
<span class="chill-entity entity-person badge-person">{{ a.hostPerson|chill_entity_render_box }}</span>
</li>
<li>
{% set address_date = date(a.startDate|date("m/d/Y")) %}
{% if a.hostPerson.getCurrentHouseholdAddress(a.endDate) is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.hostPerson.getCurrentHouseholdAddress(a.endDate)|chill_entity_render_box }}
{% endif %}
</li>
{% elseif a.hostThirdParty is not null %}
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}}</span>
<span class="chill-entity entity-person badge-person">{{ a.hostThirdParty|chill_entity_render_box }}</span>
</li>
<li>
{% if a.hostThirdParty.address is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.hostThirdParty.address|chill_entity_render_box }}
{% endif %}
</li>
{% else %}
<li>
{% if a.address is not null %}
<i class="fa fa-li fa-map-marker"></i>
{{ a.address|chill_entity_render_box }}
{% endif %}
{% endif %}
</li>
</ul>
<div class="item-row">
{% if not a.comment.isEmpty %}
{{ a.comment|chill_entity_render_box }}
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_view', { 'person_id' : person.id } ) }}" class="btn btn-cancel">
{{ 'Back to the person details'|trans }}
</a>
</li>
{% if is_granted('CHILL_PERSON_UPDATE', person) %}
<li>
<a class="btn btn-create"
href="{{ path('chill_person_residential_address_new', { 'id' : person.id } ) }}">
{{ 'Add a residential address'|trans }}
</a>
</li>
{% endif %}
</ul>
</div>
{% endblock %}

View File

@ -0,0 +1,29 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = '' %}
{% block title %}{{ 'New residential address'|trans }}{% endblock %}
{% block personcontent %}
<div class="address-new">
{% block content %}
<h1>{{ block('title') }}</h1>
{# TODO #}
{% block form %}
{% include '@ChillPerson/ResidentialAddress/_form.html.twig' %}
{% endblock %}
{% endblock %}
</div>
{% endblock %}
{% block js %}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}

View File

@ -0,0 +1,49 @@
{% extends "@ChillMain/layout.html.twig" %}
{% block title 'Which kind of residential address would you create ?'|trans %}
{% block content %}
<div class="col-10 centered">
<h1>{{ block('title') }}</h1>
<div class="container" style="margin-top: 2rem;">
<div class="row">
<div class="col-md-4 mb-4">
<a
href="{{ chill_path_forward_return_path('chill_person_residential_address_new', {'id': person.id, 'kind': 'person'}) }}"
class="btn btn-outline-chill-green-dark">
{{ 'The address of another person'|trans }}
</a>
</div>
<div class="col-md-8">
<p>{{ 'residential_address_person_explanation'|trans }}</p>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<a
href="{{ chill_path_forward_return_path('chill_person_residential_address_new', {'id': person.id, 'kind': 'thirdparty'}) }}"
class="btn btn-outline-chill-green-dark">
{{ 'The address of a third party'|trans }}
</a>
</div>
<div class="col-md-8">
<p>{{ 'residential_address_third_party_explanation'|trans }}</p>
</div>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<a
href="{{ chill_path_forward_return_path('chill_person_residential_address_new', {'id': person.id, 'kind': 'address'}) }}"
class="btn btn-outline-chill-green-dark">
{{ 'A new address'|trans }}
</a>
</div>
<div class="col-md-8">
<p>{{ 'residential_address_new_address_explanation'|trans }}</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -114,6 +114,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
$userContext = array_merge($context, ['docgen:expects' => User::class, 'groups' => 'docgen:read']);
$participationContext = array_merge($context, ['docgen:expects' => AccompanyingPeriodParticipation::class, 'groups' => 'docgen:read']);
$administrativeLocationContext = array_merge($context, ['docgen:expects' => Location::class, 'groups' => 'docgen:read']);
$workContext = array_merge($context, ['docgen:expects' => AccompanyingPeriod\AccompanyingPeriodWork::class, 'groups' => 'docgen:read']);
return [
'id' => $period->getId(),
@ -160,6 +161,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'locationPerson' => $this->normalizer->normalize($period->getPersonLocation(), $format, array_merge($context, ['docgen:expects' => Person::class])),
'location' => $this->normalizer->normalize($period->getLocation(), $format, $addressContext),
'administrativeLocation' => $this->normalizer->normalize($period->getAdministrativeLocation(), $format, $administrativeLocationContext),
'works' => $this->normalizer->normalize($period->getWorks(), $format, $workContext),
];
}
@ -178,6 +180,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'hasLocation' => false,
'hasLocationPerson' => false,
'hasAdministrativeLocation' => false,
'works' => [],
]
);
}

View File

@ -29,6 +29,7 @@ use function count;
class AccompanyingPeriodResourceNormalizer implements DenormalizerAwareInterface, DenormalizerInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
private ResourceRepository $repository;

View File

@ -33,6 +33,7 @@ use function is_array;
class AccompanyingPeriodWorkDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
public const GROUP_CREATE = 'accompanying_period_work:create';

View File

@ -76,8 +76,8 @@ class PersonDocGenNormalizer implements
'type' => 'person',
'isNull' => false,
'civility' => $this->normalizer->normalize($person->getCivility(), $format, array_merge($context, ['docgen:expects' => Civility::class])),
'firstname' => $person->getFirstName(),
'lastname' => $person->getLastName(),
'firstName' => $person->getFirstName(),
'lastName' => $person->getLastName(),
'altNames' => implode(
', ',
array_map(
@ -151,7 +151,7 @@ class PersonDocGenNormalizer implements
$normalizer = new NormalizeNullValueHelper($this->normalizer, 'type', 'person');
$attributes = [
'firstname', 'lastname', 'age', 'altNames', 'text',
'firstName', 'lastName', 'age', 'altNames', 'text',
'civility' => Civility::class,
'birthdate' => DateTimeInterface::class,
'deathdate' => DateTimeInterface::class,

View File

@ -40,7 +40,9 @@ class PersonJsonNormalizer implements
NormalizerInterface
{
use DenormalizerAwareTrait;
use NormalizerAwareTrait;
use ObjectToPopulateTrait;
private CenterResolverManagerInterface $centerResolverManager;

View File

@ -49,8 +49,11 @@ class SocialActionNormalizer implements NormalizerAwareInterface, NormalizerInte
return [
'id' => $socialAction->getId(),
'type' => 'social_work_social_action',
'text' => $this->render->renderString($socialAction, []),
'title' => $socialAction->getTitle(),
'parent' => $this->normalizer->normalize($socialAction->getParent(), $format, $context),
'issue' => $this->normalizer->normalize($socialAction->getIssue(), $format, $context),
];
default:

View File

@ -98,7 +98,7 @@ class AccompanyingPeriodWorkEvaluationContext implements
return $this->translatableStringHelper->localize($e->getTitle());
},
'multiple' => true,
'expanded' => true,
'attr' => ['class' => 'select2'],
]);
}

View File

@ -0,0 +1,52 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Templating\Entity;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\Person\PersonResourceKind;
final class ResourceKindRender implements ChillEntityRenderInterface
{
private TranslatableStringHelper $translatableStringHelper;
public function __construct(TranslatableStringHelper $translatableStringHelper)
{
$this->translatableStringHelper = $translatableStringHelper;
}
public function renderBox($entity, array $options): string
{
return
'<span class="resource">' .
$this->translatableStringHelper->localize(
$entity->getTitle()
) .
'</span>';
}
public function renderString($entity, array $options): string
{
$title = '';
if (null !== $entity->getTitle()) {
return $this->translatableStringHelper->localize($entity->getTitle());
}
return $title;
}
public function supports($entity, array $options): bool
{
return $entity instanceof PersonResourceKind;
}
}

View File

@ -29,7 +29,9 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
final class PersonVoterTest extends KernelTestCase
{
use PrepareCenterTrait;
use PrepareScopeTrait;
use PrepareUserTrait;
/**

View File

@ -100,6 +100,7 @@ final class AccompanyingPeriodDocGenNormalizerTest extends KernelTestCase
'hasLocationPerson' => false,
'location' => '@ignored',
'locationPerson' => '@ignored',
'works' => [],
];
$this->assertIsArray($data);
@ -160,6 +161,7 @@ final class AccompanyingPeriodDocGenNormalizerTest extends KernelTestCase
'hasLocationPerson' => false,
'location' => '@ignored',
'locationPerson' => '@ignored',
'works' => [],
];
$this->assertIsArray($data);

View File

@ -38,8 +38,8 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
use ProphecyTrait;
private const BLANK = [
'firstname' => '',
'lastname' => '',
'firstName' => '',
'lastName' => '',
'altNames' => '',
'text' => '',
'isNull' => true,
@ -80,7 +80,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
$expected = array_merge(
self::BLANK,
['firstname' => 'Renaud', 'lastname' => 'Mégane',
['firstName' => 'Renaud', 'lastName' => 'Mégane',
'text' => 'Renaud Mégane', ]
);

View File

@ -14,9 +14,12 @@ module.exports = function(encore, entries)
encore.addEntry('vue_accourse_work_edit', __dirname + '/Resources/public/vuejs/AccompanyingCourseWorkEdit/index.js');
encore.addEntry('vue_visgraph', __dirname + '/Resources/public/vuejs/VisGraph/index.js');
encore.addEntry('mod_set_referrer', __dirname + '/Resources/public/mod/AccompanyingPeriod/setReferrer.js');
encore.addEntry('page_household_edit_metadata', __dirname + '/Resources/public/page/household_edit_metadata/index.js');
encore.addEntry('page_person', __dirname + '/Resources/public/page/person/index.js');
encore.addEntry('page_accompanying_course_index_person_locate', __dirname + '/Resources/public/page/accompanying_course_index/person_locate.js');
encore.addEntry('page_accompanying_course_index_masonry', __dirname + '/Resources/public/page/accompanying_course_index/masonry.js');
encore.addEntry('page_person_resource_showhide_input', __dirname + '/Resources/public/page/person_resource/showhide-input.js');
encore.addEntry('page_suggest_names', __dirname + '/Resources/public/page/person/suggest-names.js');
};

View File

@ -55,6 +55,18 @@ chill_person_accompanying_period_re_open:
path: /{_locale}/person/{person_id}/accompanying-period/{period_id}/re-open
controller: Chill\PersonBundle\Controller\AccompanyingPeriodController::reOpenAction
chill_person_resource_list:
path: /{_locale}/person/{person_id}/resources/list
controller: Chill\PersonBundle\Controller\PersonResourceController::listAction
chill_person_resource_edit:
path: /{_locale}/person/{person_id}/resources/{resource_id}/edit
controller: Chill\PersonBundle\Controller\PersonResourceController::editAction
chill_person_resource_delete:
path: /{_locale}/person/{person_id}/resources/{resource_id}/delete
controller: Chill\PersonBundle\Controller\PersonResourceController::deleteAction
chill_person_address_list:
path: /{_locale}/person/{person_id}/address/list
controller: Chill\PersonBundle\Controller\PersonAddressController::listAction

View File

@ -0,0 +1,54 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Creation of person resource and person resource kind.
*/
final class Version20220119155944 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_person_resource_id_seq CASCADE');
$this->addSql('DROP SEQUENCE chill_person_resource_kind_id_seq CASCADE');
$this->addSql('DROP TABLE chill_person_resource');
$this->addSql('DROP TABLE chill_person_resource_kind');
}
public function getDescription(): string
{
return 'Creation of person resource and person resource kind';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_person_resource_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE chill_person_resource_kind_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_person_resource (id INT NOT NULL, person_id INT DEFAULT NULL, kind_id INT DEFAULT NULL, freeText TEXT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, comment_comment TEXT DEFAULT NULL, comment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, comment_userId INT DEFAULT NULL, personOwner_id INT NOT NULL, thirdParty_id INT DEFAULT NULL, createdBy_id INT NOT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_FA5B7D92D19C515B ON chill_person_resource (personOwner_id)');
$this->addSql('CREATE INDEX IDX_FA5B7D92217BBB47 ON chill_person_resource (person_id)');
$this->addSql('CREATE INDEX IDX_FA5B7D923EA5CAB0 ON chill_person_resource (thirdParty_id)');
$this->addSql('CREATE INDEX IDX_FA5B7D9230602CA9 ON chill_person_resource (kind_id)');
$this->addSql('CREATE INDEX IDX_FA5B7D923174800F ON chill_person_resource (createdBy_id)');
$this->addSql('CREATE INDEX IDX_FA5B7D9265FF1AEC ON chill_person_resource (updatedBy_id)');
$this->addSql('CREATE TABLE chill_person_resource_kind (id INT NOT NULL, title JSON NOT NULL, isActive BOOLEAN NOT NULL, PRIMARY KEY(id))');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D92D19C515B FOREIGN KEY (personOwner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D92217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D923EA5CAB0 FOREIGN KEY (thirdParty_id) REFERENCES chill_3party.third_party (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D9230602CA9 FOREIGN KEY (kind_id) REFERENCES chill_person_resource_kind (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D923174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_resource ADD CONSTRAINT FK_FA5B7D9265FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@ -121,7 +121,6 @@ address_postcode_code: Code postal
address_country_name: Pays
address_country_code: Code pays
'Alreay existing person': 'Dossiers déjà encodés'
'Add the person': 'Ajouter la personne'
'Add the person and create an accompanying period': "Créer la personne & créer une période d'accompagnement"
@ -223,9 +222,27 @@ Are you sure you want to remove the accompanying period "%id%" ?: Êtes-vous s
The accompanying course has been successfully removed.: La période d'accompagnement a été supprimée.
Concerned scopes: Services concernés
# person resource
person_resources_menu: "Personnes ressources"
Person resources: "Ressources de la personne"
Add a person resource: "Ajouter une personne ressource"
edit resource: "Modifier une personne ressource"
Remove resource: "Supprimer une personne ressource"
Are you sure you want to remove the resource for "%name%" ?: Étes-vous sûr de vouloir supprimer cette ressource de %name%?
The resource has been successfully removed.: "La ressource a été supprimée."
List of resources: "Liste des ressources"
There are no available resources: "Aucune personne ressources renseignée"
no comment found: "Aucun commentaire"
Select a type: "Choisissez un type"
Select a person: "Choisissez un usager"
Select a thirdparty: "Choisissez un tiers"
# pickAPersonType
Pick a person: Choisir une personne
# Address
No address given: Pas d'adresse renseignée
The address has been successfully updated: L'adresse a été mise à jour avec succès
Update address for %name%: Mettre à jour une adresse pour %name%
@ -240,6 +257,31 @@ The new address was created successfully: La nouvelle adresse a été créée
Add an address: Ajouter une adresse
Back to the person details: Retour aux détails de la personne
# Residential address
Residential addresses history for %name%: Historique des adresses de résidence de %name%
Residential addresses history: Historique des adresses de résidence
Add a residential address: Ajouter une adresse de résidence
Which kind of residential address would you create ?: Quel type d'adresse de résidence voulez-vous créer?
The address of another person: L'adresse d'une autre personne
The address of a third party: L'adresse d'un tiers
A new address: Une nouvelle adresse
residential_address_person_explanation: L'adresse sera positionnée auprès d'un usager. Lorsque l'usager déménage, l'adresse de résidence suivra également cet usager
residential_address_third_party_explanation: L'adresse sera associée à celle d'un tiers.
residential_address_new_address_explanation: Créer une nouvelle adresse. L'adresse sera fixe.
New residential address: Nouvelle adresse de résidence
Host person: Choisir l'adresse d'un usager
Host third party: Choisir l'adresse d'un tiers
The new residential address was created successfully: La nouvelle adresse de résidence a été créée
Edit a residential address: Modifier l'addresse de résidence
The residential address was updated successfully: L'adresse de résidence a été mise à jour
Residential addresses: Adresses de résidence
Address of: Adresse de
Delete residential address: Supprimer l'adresse de résidence
Delete residential address ?: Supprimer l'adresse de résidence ?
Are you sure you want to remove this residential address for %name% ?: Êtes-vous sûr de vouloir supprimer l'adresse de résidence pour %name% ?
Residential address had been deleted: L'adresse de résidence a été supprimée
#timeline
Timeline: Historique
Closing the accompanying period: Fermeture de la période d'accompagnement
@ -430,6 +472,7 @@ accompanying_course_comment:
Comment removed: Commentaire supprimé
Remove comment: Supprimer le commentaire
Are you sure you want to remove comment ?: Étes-vous sûr de vouloir supprimer ce commentaire ?
Read more: En savoir plus
# Household
Household: Ménage
@ -512,3 +555,6 @@ household_composition:
Currently no composition: Aucune composition famiale renseignée.
Add a composition: Ajouter une composition familiale
Update composition: Modifier la composition familiale
# docgen
Linked evaluations: Évaluations associées

View File

@ -50,3 +50,6 @@ household_membership:
A course must contains at least one social issue: 'Un parcours doit être associé à au moins une problématique sociale'
A course must be associated to at least one scope: 'Un parcours doit être associé à au moins un service'
The social %name% issue cannot be deleted because it is associated with an activity or an action: 'La problématique sociale "%name%" ne peut pas être supprimée car elle est associée à une activité ou une action'
# resource
You must associate at least one entity: Associez un usager, un tiers ou indiquez une description libre

View File

@ -28,7 +28,9 @@ use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface;
final class ReportVoterTest extends KernelTestCase
{
use PrepareCenterTrait;
use PrepareScopeTrait;
use PrepareUserTrait;
/**

View File

@ -0,0 +1,62 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\ThirdPartyBundle\Form\Type;
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Pick user dymically, using vuejs module "AddPerson".
*/
class PickThirdpartyDynamicType extends AbstractType
{
private DenormalizerInterface $denormalizer;
private SerializerInterface $serializer;
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer)
{
$this->denormalizer = $denormalizer;
$this->serializer = $serializer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'thirdparty'));
}
public function buildView(FormView $view, FormInterface $form, array $options)
{
$view->vars['multiple'] = $options['multiple'];
$view->vars['types'] = ['thirdparty'];
$view->vars['uniqid'] = uniqid('pick_user_dyn');
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('multiple', false)
->setAllowedTypes('multiple', ['bool'])
->setDefault('compound', false);
}
public function getBlockPrefix()
{
return 'pick_entity_dynamic';
}
}

View File

@ -99,7 +99,7 @@
<div class="item-col">
{% if options['isConfidential'] %}
<div class="confidential">
<div class="confidential-content blur">
<div class="confidential-content">
<ul class="list-content fa-ul">
<li>
{{ thirdparty.getAddress|chill_entity_render_box({