merge with master

This commit is contained in:
2022-03-01 14:40:14 +01:00
235 changed files with 4865 additions and 2014 deletions

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\AccompanyingPeriod\Events;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Notification;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class PersonAddressMoveEventSubscriber implements EventSubscriberInterface
{
private EngineInterface $engine;
private EntityManagerInterface $entityManager;
private Security $security;
private TranslatorInterface $translator;
public function __construct(
EngineInterface $engine,
EntityManagerInterface $entityManager,
Security $security,
TranslatorInterface $translator
) {
$this->engine = $engine;
$this->entityManager = $entityManager;
$this->security = $security;
$this->translator = $translator;
}
public static function getSubscribedEvents(): array
{
return [
PersonAddressMoveEvent::class => 'resetPeriodLocation',
];
}
public function resetPeriodLocation(PersonAddressMoveEvent $event)
{
if ($event->getPreviousAddress() !== $event->getNextAddress()
&& null !== $event->getPreviousAddress()
) {
$person = $event->getPerson();
foreach ($person->getCurrentAccompanyingPeriods() as $period) {
if ($period->getStep() === AccompanyingPeriod::STEP_DRAFT) {
continue;
}
if (
$period->getPersonLocation() === $person
&& (
$event->getMoveDate() >= $period->getLastLocationHistory()->getStartDate()
|| $event->willChangeBeActiveAt(new DateTimeImmutable('now'))
)
&& null !== $period->getUser()
&& $period->getUser() !== $this->security->getUser()
) {
// reset the location, back to an address
$period->setPersonLocation(null);
$period->setAddressLocation(Address::createFromAddress($event->getPreviousAddress()));
$notification = new Notification();
$notification
->addAddressee($period->getUser())
->setTitle($this->translator->trans('period_notification.Person locating period has moved'))
->setRelatedEntityClass(AccompanyingPeriod::class)
->setRelatedEntityId($period->getId())
->setMessage($this->engine->render('@ChillPerson/AccompanyingPeriod/notification_location_user_on_period_has_moved.fr.txt.twig', [
'oldPersonLocation' => $person,
'period' => $period,
]));
$this->entityManager->persist($notification);
}
}
}
}
}

View File

@@ -32,6 +32,7 @@ use Symfony\Component\Workflow\Registry;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_slice;
use function is_array;
/**
@@ -221,9 +222,11 @@ class AccompanyingCourseController extends Controller
$activities = $this->getDoctrine()->getManager()->getRepository(Activity::class)->findBy(
['accompanyingPeriod' => $accompanyingCourse],
['date' => 'DESC'],
['date' => 'DESC', 'id' => 'DESC'],
);
$activities = array_slice($activities, 0, 3);
$works = $this->workRepository->findByAccompanyingPeriod(
$accompanyingCourse,
['startDate' => 'DESC', 'endDate' => 'DESC'],

View File

@@ -16,28 +16,37 @@ use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\AddressReference;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use Chill\PersonBundle\Repository\Household\HouseholdACLAwareRepositoryInterface;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use DateTimeImmutable;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use function array_filter;
use function array_values;
class HouseholdApiController extends ApiController
{
private EventDispatcherInterface $eventDispatcher;
private HouseholdACLAwareRepositoryInterface $householdACLAwareRepository;
private HouseholdRepository $householdRepository;
public function __construct(
EventDispatcherInterface $eventDispatcher,
HouseholdRepository $householdRepository,
HouseholdACLAwareRepositoryInterface $householdACLAwareRepository
) {
$this->eventDispatcher = $eventDispatcher;
$this->householdRepository = $householdRepository;
$this->householdACLAwareRepository = $householdACLAwareRepository;
}
@@ -66,9 +75,51 @@ class HouseholdApiController extends ApiController
]);
}
public function householdAddressApi($id, Request $request, string $_format): Response
/**
* Add an address to a household.
*
* @Route("/api/1.0/person/household/{id}/address.{_format}", name="chill_api_single_household_address",
* methods={"POST"}, requirements={"_format": "json"})
*/
public function householdAddressApi(Household $household, Request $request, string $_format): Response
{
return $this->addRemoveSomething('address', $id, $request, $_format, 'address', Address::class, ['groups' => ['read']]);
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
/** @var Address $address */
$address = $this->getSerializer()->deserialize($request->getContent(), Address::class, $_format, [
AbstractNormalizer::GROUPS => ['write'],
]);
$household->addAddress($address);
foreach ($household->getMembersOnRange(
DateTimeImmutable::createFromMutable($address->getValidFrom()),
null === $address->getValidTo() ? null :
DateTimeImmutable::createFromMutable($address->getValidTo())
) as $member) {
/** @var HouseholdMember $member */
$event = new PersonAddressMoveEvent($member->getPerson());
$event
->setPreviousAddress($household->getPreviousAddressOf($address))
->setNextAddress($address);
dump($event);
$this->eventDispatcher->dispatch($event);
}
$errors = $this->getValidator()->validate($household);
if ($errors->count() > 0) {
return $this->json($errors, 422);
}
$this->getDoctrine()->getManager()->flush();
return $this->json(
$address,
Response::HTTP_OK,
[],
[AbstractNormalizer::GROUPS => ['read']]
);
}
/**

View File

@@ -24,7 +24,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists;
use function count;

View File

@@ -26,7 +26,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Exception;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
@@ -180,6 +180,7 @@ class HouseholdMemberController extends ApiController
public function move(Request $request, $_format): Response
{
try {
/** @var MembersEditor $editor */
$editor = $this->getSerializer()
->deserialize(
$request->getContent(),
@@ -199,6 +200,9 @@ class HouseholdMemberController extends ApiController
return $this->json($errors, 422);
}
// launch events on post move
$editor->postMove();
$em = $this->getDoctrine()->getManager();
// if new household, persist it

View File

@@ -30,8 +30,8 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
use function hash;

View File

@@ -28,7 +28,7 @@ use http\Exception\InvalidArgumentException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function count;

View File

@@ -12,12 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Doctrine\ORM\Hydration\FlatHierarchyEntityHydrator;
use Chill\MainBundle\Pagination\PaginatorInterface;
use DateTimeImmutable;
use Doctrine\ORM\Query;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
class SocialIssueApiController extends ApiController
{
@@ -32,22 +29,10 @@ class SocialIssueApiController extends ApiController
$query->setParameter('now', new DateTimeImmutable());
}
protected function getQueryResult(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query)
protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator, $_format)
{
// In order to work, this hydrator only works with
// entities having the field "children" set up.
return $query
->getQuery()
->setHint(Query::HINT_INCLUDE_META_COLUMNS, true)
->getResult(FlatHierarchyEntityHydrator::LIST);
}
$query->addOrderBy('e.ordering', 'ASC');
protected function onPostIndexBuildQuery(string $action, Request $request, string $_format, int $totalItems, PaginatorInterface $paginator, $query): ?Response
{
$query
->orderBy('GET_JSON_FIELD_BY_KEY(e.title, :locale)', 'ASC')
->setParameter(':locale', $request->getLocale());
return null;
return $query;
}
}

View File

@@ -527,15 +527,6 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
Request::METHOD_HEAD => true,
],
],
'address' => [
'methods' => [
Request::METHOD_POST => true,
Request::METHOD_DELETE => true,
Request::METHOD_GET => false,
Request::METHOD_HEAD => false,
],
'controller_action' => 'householdAddressApi',
],
'suggestHouseholdByAccompanyingPeriodParticipation' => [
'path' => '/suggest/by-person/{person_id}/through-accompanying-period-participation.{_format}',
'methods' => [

View File

@@ -21,6 +21,7 @@ use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodLocationHistory;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
@@ -37,17 +38,19 @@ use DateTimeImmutable;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Iterator;
use LogicException;
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;
use Symfony\Component\Validator\GroupSequenceProviderInterface;
use UnexpectedValueException;
use function in_array;
use const SORT_REGULAR;
/**
@@ -207,6 +210,12 @@ class AccompanyingPeriod implements
*/
private ?UserJob $job = null;
/**
* @ORM\OneToMany(targetEntity=AccompanyingPeriodLocationHistory::class,
* mappedBy="period", cascade={"persist", "remove"}, orphanRemoval=true)
*/
private Collection $locationHistories;
/**
* @var DateTime
*
@@ -379,6 +388,7 @@ class AccompanyingPeriod implements
$this->works = new ArrayCollection();
$this->resources = new ArrayCollection();
$this->userHistories = new ArrayCollection();
$this->locationHistories = new ArrayCollection();
}
/**
@@ -429,6 +439,39 @@ class AccompanyingPeriod implements
return $this;
}
public function addLocationHistory(AccompanyingPeriodLocationHistory $history): self
{
if ($this->getStep() === self::STEP_DRAFT) {
return $this;
}
if (!$this->locationHistories->contains($history)) {
$this->locationHistories[] = $history;
$history->setPeriod($this);
}
// ensure continuity of histories
$criteria = new Criteria();
$criteria->orderBy(['startDate' => Criteria::ASC, 'id' => Criteria::ASC]);
/** @var Iterator $locations */
$locations = $this->getLocationHistories()->matching($criteria)->getIterator();
$locations->rewind();
do {
/** @var AccompanyingPeriodLocationHistory $current */
$current = $locations->current();
$locations->next();
if ($locations->valid()) {
$next = $locations->current();
$current->setEndDate($next->getStartDate());
}
} while ($locations->valid());
return $this;
}
public function addPerson(?Person $person = null): self
{
if (null !== $person) {
@@ -661,6 +704,17 @@ class AccompanyingPeriod implements
return $this->job;
}
public function getLastLocationHistory(): ?AccompanyingPeriodLocationHistory
{
foreach ($this->getLocationHistories() as $locationHistory) {
if (null === $locationHistory->getEndDate()) {
return $locationHistory;
}
}
return null;
}
/**
* Get the location, taking precedence into account.
*
@@ -675,6 +729,14 @@ class AccompanyingPeriod implements
return $this->getAddressLocation();
}
/**
* @return Collection|AccompanyingPeriodLocationHistory[]
*/
public function getLocationHistories(): Collection
{
return $this->locationHistories;
}
/**
* Get where the location is.
*
@@ -977,6 +1039,15 @@ class AccompanyingPeriod implements
$this->comments->removeElement($comment);
}
public function removeLocationHistory(AccompanyingPeriodLocationHistory $history): self
{
if ($this->locationHistories->removeElement($history)) {
$history->setPeriod(null);
}
return $this;
}
/**
* Remove Participation.
*/
@@ -1031,7 +1102,18 @@ class AccompanyingPeriod implements
*/
public function setAddressLocation(?Address $addressLocation = null): self
{
$this->addressLocation = $addressLocation;
if ($this->addressLocation !== $addressLocation) {
$this->addressLocation = $addressLocation;
if (null !== $addressLocation) {
$locationHistory = new AccompanyingPeriodLocationHistory();
$locationHistory
->setStartDate(new DateTimeImmutable('now'))
->setAddressLocation($addressLocation);
$this->addLocationHistory($locationHistory);
}
}
return $this;
}
@@ -1134,7 +1216,18 @@ class AccompanyingPeriod implements
*/
public function setPersonLocation(?Person $person = null): self
{
$this->personLocation = $person;
if ($this->personLocation !== $person) {
$this->personLocation = $person;
if (null !== $person) {
$locationHistory = new AccompanyingPeriodLocationHistory();
$locationHistory
->setStartDate(new DateTimeImmutable('now'))
->setPersonLocation($person);
$this->addLocationHistory($locationHistory);
}
}
return $this;
}
@@ -1201,8 +1294,14 @@ class AccompanyingPeriod implements
public function setStep(string $step): self
{
$previous = $this->step;
$this->step = $step;
if (self::STEP_DRAFT === $previous && self::STEP_DRAFT !== $step) {
$this->bootPeriod();
}
return $this;
}
@@ -1241,6 +1340,17 @@ class AccompanyingPeriod implements
return $this;
}
private function bootPeriod(): void
{
// first location history
$locationHistory = new AccompanyingPeriodLocationHistory();
$locationHistory
->setStartDate(new DateTimeImmutable('now'))
->setPersonLocation($this->getPersonLocation())
->setAddressLocation($this->getAddressLocation());
$this->addLocationHistory($locationHistory);
}
private function setRequestorPerson(?Person $requestorPerson = null): self
{
$this->requestorPerson = $requestorPerson;

View File

@@ -0,0 +1,129 @@
<?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\AccompanyingPeriod;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("chill_person_accompanying_period_location_history")
*/
class AccompanyingPeriodLocationHistory implements TrackCreationInterface
{
use TrackCreationTrait;
/**
* @ORM\ManyToOne(targetEntity=Address::class, cascade={"persist"})
*/
private ?Address $addressLocation = null;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private ?int $id = null;
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
*/
private AccompanyingPeriod $period;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
*/
private ?Person $personLocation = null;
/**
* @ORM\Column(type="date_immutable")
*/
private ?DateTimeImmutable $startDate = null;
public function getAddressLocation(): ?Address
{
return $this->addressLocation;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getId(): ?int
{
return $this->id;
}
public function getPeriod(): AccompanyingPeriod
{
return $this->period;
}
public function getPersonLocation(): ?Person
{
return $this->personLocation;
}
public function getStartDate(): ?DateTimeImmutable
{
return $this->startDate;
}
public function setAddressLocation(?Address $addressLocation): AccompanyingPeriodLocationHistory
{
$this->addressLocation = $addressLocation;
return $this;
}
public function setEndDate(?DateTimeImmutable $endDate): AccompanyingPeriodLocationHistory
{
$this->endDate = $endDate;
return $this;
}
/**
* @internal use AccompanyingPeriod::addLocationHistory
*/
public function setPeriod(AccompanyingPeriod $period): AccompanyingPeriodLocationHistory
{
$this->period = $period;
return $this;
}
public function setPersonLocation(?Person $personLocation): AccompanyingPeriodLocationHistory
{
$this->personLocation = $personLocation;
return $this;
}
public function setStartDate(?DateTimeImmutable $startDate): AccompanyingPeriodLocationHistory
{
$this->startDate = $startDate;
return $this;
}
}

View File

@@ -250,10 +250,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this->accompanyingPeriod;
}
/**
* @return Collection
*/
public function getAccompanyingPeriodWorkEvaluations()
public function getAccompanyingPeriodWorkEvaluations(): Collection
{
return $this->accompanyingPeriodWorkEvaluations;
}

View File

@@ -67,10 +67,15 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
private ?User $createdBy = null;
/**
* **Note on deserialization/denormalization**: denormalization of documents is handled by.
*
* @see{Chill\PersonBundle\Serializer\Normalizer\AccompanyingPeriodWorkEvaluationDenormalizer}
*
* @ORM\OneToMany(
* targetEntity=AccompanyingPeriodWorkEvaluationDocument::class,
* mappedBy="accompanyingPeriodWorkEvaluation",
* cascade={"remove"}
* cascade={"remove", "persist"},
* orphanRemoval=true
* )
* @Serializer\Groups({"read"})
*/
@@ -261,6 +266,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
public function removeDocument(AccompanyingPeriodWorkEvaluationDocument $document): self
{
$this->documents->removeElement($document);
$document->setAccompanyingPeriodWorkEvaluation(null);
return $this;
}

View File

@@ -41,6 +41,7 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?\DateTimeImmutable $createdAt = null;
@@ -49,6 +50,7 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
* targetEntity=User::class
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?User $createdBy = null;
@@ -60,15 +62,32 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
* @internal the default name exceeds 64 characters, we must set manually:
* @ORM\SequenceGenerator(sequenceName="chill_person_social_work_eval_doc_id_seq", allocationSize=1, initialValue=1000)
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?int $id;
private ?int $id = null;
/**
* This is a workaround for client, to allow them to assign arbitrary data
* dedicated to their job.
*
* This data is not persisted into database, but will appears on the data
* normalized during the same request (like PUT/PATCH request)
*
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*
* @var mixed
*/
private $key;
/**
* @ORM\ManyToOne(
* targetEntity=StoredObject::class,
* cascade={"remove"},
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?StoredObject $storedObject = null;
@@ -77,12 +96,22 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
* targetEntity=DocGeneratorTemplate::class
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?DocGeneratorTemplate $template = null;
/**
* @ORM\Column(type="text", nullable=false, options={"default": ""})
* @Serializer\Groups({"read"})
* @Serializer\Groups({"write"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?string $title = '';
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?\DateTimeImmutable $updatedAt = null;
@@ -91,6 +120,7 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
* targetEntity=User::class
* )
* @Serializer\Groups({"read"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?User $updatedBy = null;
@@ -117,6 +147,14 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
return $this->id;
}
/**
* @return mixed
*/
public function getKey()
{
return $this->key;
}
public function getStoredObject(): ?StoredObject
{
return $this->storedObject;
@@ -127,6 +165,11 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
return $this->template;
}
public function getTitle(): ?string
{
return $this->title;
}
/**
* @return DateTimeImmutable|null
*/
@@ -171,6 +214,18 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
return $this;
}
/**
* @param mixed $key
*
* @return AccompanyingPeriodWorkEvaluationDocument
*/
public function setKey($key)
{
$this->key = $key;
return $this;
}
public function setStoredObject(?StoredObject $storedObject): AccompanyingPeriodWorkEvaluationDocument
{
$this->storedObject = $storedObject;
@@ -185,6 +240,13 @@ class AccompanyingPeriodWorkEvaluationDocument implements \Chill\MainBundle\Doct
return $this;
}
public function setTitle(?string $title): AccompanyingPeriodWorkEvaluationDocument
{
$this->title = $title;
return $this;
}
public function setUpdatedAt(DateTimeInterface $datetime): TrackUpdateInterface
{
$this->updatedAt = $datetime;

View File

@@ -66,6 +66,7 @@ class AccompanyingPeriodParticipation
$this->startDate = new DateTime('now');
$this->accompanyingPeriod = $accompanyingPeriod;
$this->person = $person;
$person->getAccompanyingPeriodParticipations()->add($this);
}
public function getAccompanyingPeriod(): ?AccompanyingPeriod

View File

@@ -106,19 +106,13 @@ class Household
$this->compositions = new ArrayCollection();
}
/**
* @return $this
*/
public function addAddress(Address $address)
public function addAddress(Address $address): self
{
foreach ($this->getAddresses() as $a) {
if ($a->getValidFrom() <= $address->getValidFrom() && $a->getValidTo() === null) {
$a->setValidTo($address->getValidFrom());
}
if (!$this->addresses->contains($address)) {
$this->addresses[] = $address;
$this->makeAddressConsistent();
}
$this->addresses[] = $address;
return $this;
}
@@ -157,6 +151,31 @@ class Household
return $this->addresses;
}
public function getAddressesOrdered(): array
{
$addresses = $this->getAddresses()->toArray();
usort($addresses, static function (Address $a, Address $b) {
$validFromA = $a->getValidFrom()->format('Y-m-d');
$validFromB = $b->getValidFrom()->format('Y-m-d');
if ($a === $b) {
if (null === $a->getId()) {
return 1;
}
if (null === $b->getId()) {
return -1;
}
return $a->getId() <=> $b->getId();
}
return $validFromA <=> $validFromB;
});
return $addresses;
}
public function getCommentMembers(): CommentEmbeddable
{
return $this->commentMembers;
@@ -339,7 +358,7 @@ class Household
$membership->getEndDate()
)->filter(
static function (HouseholdMember $m) use ($membership) {
return $m !== $membership;
return $m->getPerson() !== $membership->getPerson();
}
);
}
@@ -358,24 +377,25 @@ class Household
public function getMembersOnRange(DateTimeImmutable $from, ?DateTimeImmutable $to): Collection
{
$criteria = new Criteria();
$expr = Criteria::expr();
return $this->getMembers()->filter(static function (HouseholdMember $m) use ($from, $to) {
if (null === $m->getEndDate() && null !== $to) {
return $m->getStartDate() <= $to;
}
$criteria->where(
$expr->gte('startDate', $from)
);
if (null === $to) {
return $m->getStartDate() >= $from || null === $m->getEndDate();
}
if (null !== $to) {
$criteria->andWhere(
$expr->orX(
$expr->lte('endDate', $to),
$expr->eq('endDate', null)
),
);
}
if (null !== $m->getEndDate() && $m->getEndDate() < $from) {
return false;
}
return $this->getMembers()
->matching($criteria);
if ($m->getStartDate() <= $to) {
return true;
}
return false;
});
}
public function getNonCurrentMembers(?DateTimeImmutable $now = null): Collection
@@ -418,6 +438,25 @@ class Household
return $this->getNonCurrentMembers($now)->matching($criteria);
}
public function getPreviousAddressOf(Address $address): ?Address
{
$iterator = new ArrayIterator($this->getAddressesOrdered());
$iterator->rewind();
while ($iterator->valid()) {
$current = $iterator->current();
$iterator->next();
if ($iterator->valid()) {
if ($iterator->current() === $address) {
return $current;
}
}
}
return null;
}
public function getWaitingForBirth(): bool
{
return $this->waitingForBirth;
@@ -462,6 +501,23 @@ class Household
} while ($iterator->valid());
}
public function makeAddressConsistent(): void
{
$iterator = new ArrayIterator($this->getAddressesOrdered());
$iterator->rewind();
while ($iterator->valid()) {
$current = $iterator->current();
$iterator->next();
if ($iterator->valid()) {
$current->setValidTo($iterator->current()->getValidFrom());
}
}
}
public function removeAddress(Address $address)
{
$this->addresses->removeElement($address);

View File

@@ -930,6 +930,8 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/**
* Get current accompanyingPeriods array.
*
* @return AccompanyingPeriod[]|array
*/
public function getCurrentAccompanyingPeriods(): array
{

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\Event\Person;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use DateTime;
use DateTimeImmutable;
use Symfony\Contracts\EventDispatcher\Event;
class PersonAddressMoveEvent extends Event
{
public const PERSON_MOVE_POST = 'chill_person.person_move_post';
private ?Address $nextAddress = null;
private ?HouseholdMember $nextMembership = null;
private Person $person;
private ?Address $previousAddress = null;
private ?HouseholdMember $previousMembership = null;
public function __construct(
Person $person
) {
$this->person = $person;
}
/**
* Get the date of the move.
*
* It might be either:
*
* * the date of the new membership;
* * or the date of the move
* * or the date when the household leaving take place (the end date of the previous membership)
*/
public function getMoveDate(): DateTimeImmutable
{
if ($this->personLeaveWithoutHousehold()) {
return $this->getPreviousMembership()->getEndDate();
}
if ($this->personChangeHousehold()) {
return $this->getNextMembership()->getStartDate();
}
// person is changing address without household
return DateTimeImmutable::createFromMutable($this->getNextAddress()->getValidFrom());
}
public function getNextAddress(): ?Address
{
if (null !== $this->getNextMembership()) {
return $this->getNextMembership()->getHousehold()
->getCurrentAddress(
$this->getMoveDate() === null ? null :
DateTime::createFromImmutable($this->getMoveDate())
);
}
return $this->nextAddress;
}
public function getNextHousehold(): ?Household
{
if (null !== $nextMembership = $this->getNextMembership()) {
return $nextMembership->getHousehold();
}
return null;
}
public function getNextMembership(): ?HouseholdMember
{
return $this->nextMembership;
}
public function getPerson(): Person
{
return $this->person;
}
public function getPreviousAddress(): ?Address
{
if (null !== $this->getPreviousMembership()) {
return $this->getPreviousMembership()->getHousehold()
->getCurrentAddress(
null === $this->getMoveDate() ? null :
DateTime::createFromImmutable($this->getMoveDate())
);
}
return $this->previousAddress;
}
public function getPreviousHousehold(): ?Household
{
if (null !== $previousMembership = $this->getPreviousMembership()) {
return $previousMembership->getHousehold();
}
return null;
}
public function getPreviousMembership(): ?HouseholdMember
{
return $this->previousMembership;
}
public function personChangeAddress(): bool
{
return $this->getPreviousAddress() !== $this->getNextAddress();
}
/**
* Return true if the user change household (this include the fact that a person
* leave household without a new one).
*/
public function personChangeHousehold(): bool
{
return $this->getPreviousHousehold() !== $this->getNextHousehold();
}
public function personLeaveWithoutHousehold(): bool
{
return null === $this->getNextMembership()
&& null === $this->getNextAddress();
}
public function setNextAddress(?Address $nextAddress): PersonAddressMoveEvent
{
$this->nextAddress = $nextAddress;
return $this;
}
public function setNextMembership(?HouseholdMember $nextMembership): PersonAddressMoveEvent
{
$this->nextMembership = $nextMembership;
return $this;
}
public function setPreviousAddress(?Address $previousAddress): PersonAddressMoveEvent
{
$this->previousAddress = $previousAddress;
return $this;
}
public function setPreviousMembership(?HouseholdMember $previousMembership): PersonAddressMoveEvent
{
$this->previousMembership = $previousMembership;
return $this;
}
/**
* Will the change affect this date ?
*/
public function willChangeBeActiveAt(DateTimeImmutable $date): bool
{
if ($this->getMoveDate() < $date && $this->personLeaveWithoutHousehold()) {
return true;
}
if ($this->personChangeHousehold()) {
if ($this->getMoveDate() > $date) {
return false;
}
if (null === $this->getNextMembership()->getEndDate()) {
return true;
}
if ($this->getNextMembership()->getEndDate() > $date) {
return true;
}
} else {
if ($this->getNextAddress()->getValidFrom() > $date) {
return false;
}
if (null === $this->getNextAddress()->getValidTo()) {
return true;
}
if ($this->getNextAddress()->getValidTo() > $date) {
return true;
}
}
return false;
}
}

View File

@@ -20,8 +20,8 @@ use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class CountryOfBirthAggregator implements AggregatorInterface, ExportElementValidatedInterface
{

View File

@@ -17,7 +17,7 @@ use Chill\PersonBundle\Export\Declarations;
use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class GenderAggregator implements AggregatorInterface
{

View File

@@ -20,8 +20,8 @@ use Doctrine\ORM\QueryBuilder;
use LogicException;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
final class NationalityAggregator implements AggregatorInterface, ExportElementValidatedInterface
{

View File

@@ -29,9 +29,9 @@ use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function addcslashes;
use function array_key_exists;

View File

@@ -24,8 +24,8 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Render a list of duplicate peoples.
@@ -67,7 +67,7 @@ class ListPersonDuplicate implements DirectExportInterface, ExportElementValidat
$this->translator = $translator;
$this->router = $router;
$this->baseUrl = $routeParameters['scheme'] .
'://' . $routeParameters['host'];
'://' . $routeParameters['host'];
}
public function buildForm(FormBuilderInterface $builder)

View File

@@ -18,8 +18,8 @@ use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_filter;
use function count;

View File

@@ -25,6 +25,13 @@ class HouseholdMemberType extends AbstractType
'label' => 'household.Start date',
'input' => 'datetime_immutable',
]);
if (!$options['data']->getPosition()->getShareHousehold()) {
$builder->add('endDate', ChillDateType::class, [
'label' => 'household.End date',
'input' => 'datetime_immutable',
]);
}
$builder
->add('comment', ChillTextareaType::class, [
'label' => 'household.Comment',

View File

@@ -16,7 +16,7 @@ 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\PersonRenderInterface;
use Chill\PersonBundle\Templating\Entity\ResourceKindRender;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
@@ -29,7 +29,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
final class PersonResourceType extends AbstractType
{
private PersonRender $personRender;
private PersonRenderInterface $personRender;
private ResourceKindRender $resourceKindRender;
@@ -37,7 +37,7 @@ final class PersonResourceType extends AbstractType
private TranslatorInterface $translator;
public function __construct(ResourceKindRender $resourceKindRender, PersonRender $personRender, ThirdPartyRender $thirdPartyRender, TranslatorInterface $translator)
public function __construct(ResourceKindRender $resourceKindRender, PersonRenderInterface $personRender, ThirdPartyRender $thirdPartyRender, TranslatorInterface $translator)
{
$this->resourceKindRender = $resourceKindRender;
$this->personRender = $personRender;

View File

@@ -12,11 +12,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Form;
use Chill\CustomFieldsBundle\Form\Type\CustomFieldType;
use Chill\MainBundle\Entity\Civility;
use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\MainBundle\Form\Type\Select2CountryType;
use Chill\MainBundle\Form\Type\Select2LanguageType;
use Chill\MainBundle\Templating\TranslatableStringHelper;
@@ -27,9 +27,6 @@ use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PersonPhoneType;
use Chill\PersonBundle\Form\Type\Select2MaritalStatusType;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
@@ -190,19 +187,10 @@ class PersonType extends AbstractType
if ('visible' === $this->config['civility']) {
$builder
->add('civility', EntityType::class, [
'label' => 'Civility',
'class' => Civility::class,
'choice_label' => function (Civility $civility): string {
return $this->translatableStringHelper->localize($civility->getName());
},
'query_builder' => static function (EntityRepository $er): QueryBuilder {
return $er->createQueryBuilder('c')
->where('c.active = true')
->orderBy('c.order');
},
'placeholder' => 'choose civility',
->add('civility', PickCivilityType::class, [
'required' => false,
'label' => 'Civility',
'placeholder' => 'choose civility',
]);
}

View File

@@ -27,7 +27,7 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function in_array;
use function is_array;

View File

@@ -15,13 +15,14 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use DateTimeImmutable;
use Doctrine\Common\Collections\Criteria;
use LogicException;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use function in_array;
use function spl_object_hash;
@@ -33,6 +34,10 @@ class MembersEditor
public const VALIDATION_GROUP_CREATED = 'household_memberships_created';
private EventDispatcherInterface $eventDispatcher;
private array $events = [];
private ?Household $household = null;
private array $membershipsAffected = [];
@@ -43,10 +48,11 @@ class MembersEditor
private ValidatorInterface $validator;
public function __construct(ValidatorInterface $validator, ?Household $household)
public function __construct(ValidatorInterface $validator, ?Household $household, EventDispatcherInterface $eventDispatcher)
{
$this->validator = $validator;
$this->household = $household;
$this->eventDispatcher = $eventDispatcher;
}
public function addMovement(DateTimeImmutable $date, Person $person, Position $position, ?bool $holder = false, ?string $comment = null): self
@@ -55,6 +61,8 @@ class MembersEditor
throw new LogicException('You must define a household first');
}
$event = new PersonAddressMoveEvent($person);
$membership = (new HouseholdMember())
->setStartDate($date)
->setPerson($person)
@@ -62,6 +70,7 @@ class MembersEditor
->setHolder($holder)
->setComment($comment);
$this->household->addMember($membership);
$event->setNextMembership($membership);
if ($position->getShareHousehold()) {
foreach ($person->getHouseholdParticipationsShareHousehold() as $participation) {
@@ -74,6 +83,7 @@ class MembersEditor
}
if ($participation->getEndDate() === null || $participation->getEndDate() > $date) {
$event->setPreviousMembership($participation);
$participation->setEndDate($date);
$this->membershipsAffected[] = $participation;
$this->oldMembershipsHashes[] = spl_object_hash($participation);
@@ -92,6 +102,7 @@ class MembersEditor
$this->membershipsAffected[] = $membership;
$this->persistables[] = $membership;
$this->events[] = $event;
return $this;
}
@@ -129,6 +140,8 @@ class MembersEditor
->matching($criteria);
foreach ($participations as $participation) {
$this->events[] = $event = new PersonAddressMoveEvent($person);
$event->setPreviousMembership($participation);
$participation->setEndDate($date);
$this->membershipsAffected[] = $participation;
}
@@ -136,6 +149,13 @@ class MembersEditor
return $this;
}
public function postMove(): void
{
foreach ($this->events as $event) {
$this->eventDispatcher->dispatch($event);
}
}
public function validate(): ConstraintViolationListInterface
{
if ($this->hasHousehold()) {

View File

@@ -13,16 +13,24 @@ namespace Chill\PersonBundle\Household;
use Chill\PersonBundle\Entity\Household\Household;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class MembersEditorFactory
{
public function __construct(ValidatorInterface $validator)
{
private EventDispatcherInterface $eventDispatcher;
private ValidatorInterface $validator;
public function __construct(
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator
) {
$this->validator = $validator;
$this->eventDispatcher = $eventDispatcher;
}
public function createEditor(?Household $household = null): MembersEditor
{
return new MembersEditor($this->validator, $household);
return new MembersEditor($this->validator, $household, $this->eventDispatcher);
}
}

View File

@@ -72,6 +72,16 @@ class PersonMenuBuilder implements LocalMenuBuilderInterface
'order' => 60,
]);
$menu->addChild($this->translator->trans('person_resources_menu'), [
'route' => 'chill_person_resource_list',
'routeParameters' => [
'person_id' => $parameters['person']->getId(),
],
])
->setExtras([
'order' => 70,
]);
$menu->addChild($this->translator->trans('household.person history'), [
'route' => 'chill_person_household_person_history',
'routeParameters' => [
@@ -106,16 +116,6 @@ 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

@@ -15,7 +15,7 @@ use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Class SectionMenuBuilder.

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\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\Persistence\ObjectRepository;
class AccompanyingPeriodWorkEvaluationDocumentRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $em)
{
$this->repository = $em->getRepository(AccompanyingPeriodWorkEvaluationDocument::class);
}
public function find($id): ?AccompanyingPeriodWorkEvaluationDocument
{
return $this->repository->find($id);
}
/**
* @return array|object[]|AccompanyingPeriodWorkEvaluationDocument[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param null|mixed $limit
* @param null|mixed $offset
*
* @return array|object[]|AccompanyingPeriodWorkEvaluationDocument[]
*/
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): ?AccompanyingPeriodWorkEvaluationDocument
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return AccompanyingPeriodWorkEvaluationDocument::class;
}
}

View File

@@ -20,7 +20,6 @@
<div class="form-check" v-for="p in participationWithoutHousehold" :key="p.id">
<input type="checkbox"
class="form-check-input"
v-model="hasNoHousehold"
name="persons[]"
checked="checked"
:id="p.person.id"

View File

@@ -97,16 +97,10 @@ export default {
...mapGetters(['isJobValid', 'usersSuggestedFilteredByJob']),
users: function () {
let users = this.$store.getters.usersFilteredByJob;
console.log('users filtered by job', users);
// ensure that the selected user is in the list. add it if necessary
if (this.$store.state.accompanyingCourse.user !== null && users.find(u => this.$store.state.accompanyingCourse.user.id === u.id) === undefined) {
console.log('add user to users');
users.push(this.$store.state.accompanyingCourse.user);
}
console.log('users to return', users);
return users;
},
valueJob: {

View File

@@ -6,7 +6,7 @@
<div v-if="accompanyingCourse.requestor && isAnonymous" class="flex-table">
<label>
<input type="checkbox" v-model="isAnonymous" class="me-2" />
<input type="checkbox" v-model="requestorIsAnonymous" class="me-2" />
{{ $t('requestor.is_anonymous') }}
</label>
<confidential v-if="accompanyingCourse.requestor.type === 'thirdparty'">
@@ -72,7 +72,7 @@
<div v-else-if="accompanyingCourse.requestor && !isAnonymous" class="flex-table">
<label>
<input type="checkbox" v-model="isAnonymous" class="me-2" />
<input type="checkbox" v-model="requestorIsAnonymous" class="me-2" />
{{ $t('requestor.is_anonymous') }}
</label>
@@ -199,7 +199,7 @@ export default {
...mapState({
suggestedEntities: state => {
return [
...state.accompanyingCourse.participations.map(p => p.person),
...state.accompanyingCourse.participations.filter((p) => p.endDate === null).map((p) => p.person),
...state.accompanyingCourse.resources.map(r => r.resource)
]
.filter((e) => e !== null)
@@ -218,7 +218,7 @@ export default {
accompanyingCourse() {
return this.$store.state.accompanyingCourse
},
isAnonymous: {
requestorIsAnonymous: {
set(value) {
this.$store.dispatch('requestorIsAnonymous', value);
},
@@ -287,7 +287,9 @@ export default {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
if (payload.data.address) {
body.address = { id: payload.data.address.address_id };
}
makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => {

View File

@@ -77,7 +77,7 @@ export default {
counter: state => state.accompanyingCourse.resources.length,
suggestedEntities: state => [
state.accompanyingCourse.requestor,
...state.accompanyingCourse.participations.map(p => p.person),
...state.accompanyingCourse.participations.filter((p) => p.endDate === null).map((p) => p.person),
]
.filter((e) => e !== null)
.filter(

View File

@@ -159,18 +159,19 @@ export default {
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
this.$toast.open({message: 'An error occurred'});
}
})
}
else if (payload.type === 'thirdparty') {
console.log('data', payload.data)
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
body.address = payload.data.address ? { id: payload.data.address.address_id } : null;
makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)
.then(response => {
@@ -180,10 +181,10 @@ export default {
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
this.$toast.open({message: 'An error occurred'});
}
})
}

View File

@@ -428,42 +428,6 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
throw error;
})
},
/**
* On The Fly
*/
patchOnTheFly({ commit }, payload) {
// TODO should be into the dedicated component, no ? JF
console.log('## action: patch OnTheFly', payload);
let body = { type: payload.type };
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
console.log('id', payload.data.id, 'and body', body);
patchPerson(payload.data.id, body)
.then(person => new Promise((resolve, reject) => {
console.log('patch person', person);
commit('updatePerson', { target: payload.target, person: person });
resolve();
}));
}
else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
console.log('id', payload.data.id, 'and body', body);
patchThirdparty(payload.data.id, body)
.then(thirdparty => new Promise((resolve, reject) => {
console.log('patch thirdparty', thirdparty);
commit('updateThirdparty', { target: payload.target, thirdparty: thirdparty });
resolve();
}));
}
},
/**
* Update accompanying course intensity/emergency/confidentiality
*/

View File

@@ -251,6 +251,7 @@
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork"
:relatedEntityId="this.work.id"
:workflowsAvailables="this.work.workflows_availables"
:preventDefaultMoveToGenerate="true"
@go-to-generate-workflow="goToGenerateWorkflow"
></list-workflow-modal>
</li>

View File

@@ -106,8 +106,6 @@ export default {
this.toggleEditEvaluation();
},
goToGenerateWorkflow({event, link, workflowName}) {
console.log('goToGenerate in evaluation', event, link, workflowName);
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
window.location.assign(buildLinkCreate(workflowName,

View File

@@ -65,29 +65,55 @@
<h5>{{ $t('Documents') }} :</h5>
<div class="flex-table">
<div class="item-bloc" v-for="d in evaluation.documents">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.key">
<div class="item-row">
<div class="item-col"><h6>{{ d.template.name.fr }}</h6></div>
<div class="item-col">
<p>Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div>
<div class="input-group input-group-lg mb-3">
<div>
<input
class="form-control form-control-lg"
style="font-weight: bold;"
type="text"
:value="d.title"
:id="d.id"
@input="onInputDocumentTitle"/>
</div>
</div>
</div>
<div class="item-row">
<ul class="record_actions" >
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-action btn-sm">
<i class="fa fa-edit"></i>
</a>
</li>
</ul>
<div class="item-col item-meta">
<p v-if="d.createdBy" class="createdBy">Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div>
<div class="item-col">
<ul class="record_actions" >
<li v-if="d.workflows_availables.length > 0">
<list-workflow-modal
:workflows="d.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument"
:relatedEntityId="d.id"
:workflowsAvailables="d.workflows_availables"
:preventDefaultMoveToGenerate="true"
:goToGenerateWorkflowPayload="{doc: d}"
@go-to-generate-workflow="goToGenerateWorkflowEvaluationDocument"
></list-workflow-modal>
</li>
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a>
</li>
<li>
<a class="btn btn-delete" @click="removeDocument(d)">
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<h6>{{ $t('document_add') }} :</h6>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id"
@@ -99,6 +125,20 @@
<label class="col-sm-4 col-form-label">{{ $t('evaluation_generate_a_document') }}</label>
</template>
</pick-template>
<div>
<label class="col-sm-4 col-form-label">{{ $t('document_upload') }}</label>
<ul class="record_actions">
<li>
<add-async-upload
:buttonTitle="$t('browse')"
:options="asyncUploadOptions"
@addDocument="addDocument"
>
</add-async-upload>
</li>
</ul>
</div>
</div>
</div>
</div>
@@ -111,6 +151,9 @@ import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
import AddAsyncUpload from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUpload.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
const i18n = {
messages: {
@@ -129,6 +172,11 @@ const i18n = {
evaluation_add_a_document: "Ajouter un document",
evaluation_add: "Ajouter une évaluation",
Documents: "Documents",
document_add: "Générer ou téléverser un document",
document_upload: "Téléverser un document",
document_title: "Titre du document",
template_title: "Nom du template",
browse: "Ajouter un document"
}
}
};
@@ -139,12 +187,19 @@ export default {
components: {
ckeditor: CKEditor.component,
PickTemplate,
AddAsyncUpload,
ListWorkflowModal,
},
i18n,
data() {
return {
editor: ClassicEditor,
template: null,
asyncUploadOptions: {
maxFiles: 1,
maxPostSize: 15000000,
required: false,
}
}
},
computed: {
@@ -162,7 +217,6 @@ export default {
return dateToISO(this.evaluation.startDate);
},
set(v) {
console.log(v);
this.$store.commit('setEvaluationStartDate', { key: this.evaluation.key, date: ISOToDate(v) });
}
},
@@ -205,11 +259,11 @@ export default {
})
;
},
buildEditLink(storedObject) {
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
buildEditLink(storedObject) {
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash);
},
submitBeforeGenerate({template}) {
},
submitBeforeGenerate({template}) {
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
@@ -217,7 +271,36 @@ export default {
};
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });
}
},
onInputDocumentTitle(event) {
const id = Number(event.target.id);
const title = event.target.value;
this.$store.commit('updateDocumentTitle', {id: id, evaluationKey: this.evaluation.key, title: title});
},
addDocument(storedObject) {
let document = {
type: 'accompanying_period_work_evaluation_document',
storedObject: storedObject,
title: 'Nouveau document',
};
this.$store.commit('addDocument', {key: this.evaluation.key, document: document});
},
removeDocument(document) {
if (window.confirm("Êtes-vous sûr·e de vouloir supprimer le document qui a pour titre \"" + document.title +"\" ?")) {
this.$store.commit('removeDocument', {key: this.evaluation.key, document: document});
}
},
goToGenerateWorkflowEvaluationDocument({event, link, workflowName, payload}) {
const callback = (data) => {
let evaluation = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key);
let updatedDocument = evaluation.documents.find(d => d.key === payload.doc.key);
window.location.assign(buildLinkCreate(workflowName,
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', updatedDocument.id));
};
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
},
},
}
</script>

View File

@@ -110,6 +110,7 @@ const store = createStore({
maxDate: e.maxDate !== null ? { datetime: datetimeToISO(e.maxDate) } : null,
warningInterval: intervalDaysToISO(e.warningInterval),
comment: e.comment,
documents: e.documents
};
if (e.id !== undefined) {
o.id = e.id;
@@ -130,6 +131,11 @@ const store = createStore({
endDate: e.endDate !== null ? ISOToDatetime(e.endDate.datetime) : null,
maxDate: e.maxDate !== null ? ISOToDatetime(e.maxDate.datetime) : null,
warningInterval: e.warningInterval !== null ? intervalISOToDays(e.warningInterval) : null,
documents: e.documents.map((d, dindex) => {
return Object.assign(d, {
key: index
});
}),
});
return k;
@@ -197,6 +203,24 @@ const store = createStore({
found.results = found.results.filter(r => r.id !== result.id);
},
addDocument(state, payload) {
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
evaluation.documents.push(Object.assign(
payload.document, {
key: evaluation.documents.length + 1,
workflows_availables: state.work.workflows_availables_evaluation_documents,
workflows: [],
}));
},
removeDocument(state, {key, document}) {
let evaluations = state.evaluationsPicked.find(e => e.key === key);
if (evaluations === undefined) {
return;
}
evaluations.documents = evaluations.documents.filter(d => d.key !== document.key);
},
addEvaluation(state, evaluation) {
let e = {
type: "accompanying_period_work_evaluation",
@@ -284,6 +308,10 @@ const store = createStore({
setIsPosting(state, st) {
state.isPosting = st;
},
updateDocumentTitle(state, payload) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.id === payload.id).title = payload.title;
}
},
actions: {
updateThirdParty({ commit }, payload) {
@@ -374,13 +402,18 @@ const store = createStore({
});
}
},
addDocument({commit}, payload) {
commit('addDocument', payload);
},
removeDocument({commit}, payload) {
commit('removeDocument', payload);
},
submit({ getters, state, commit }, callback) {
let
payload = getters.buildPayload,
url = `/api/1.0/person/accompanying-course/work/${state.work.id}.json`,
errors = []
;
commit('setIsPosting', true);
return makeFetch('PUT', url, payload)
@@ -397,36 +430,9 @@ const store = createStore({
commit('setErrors', error.violations);
});
},
patchOnTheFly({ commit }, payload) {
let body = { type: payload.type };
const id = payload.data.id;
let url = `/api/1.0/person/person/${id}.json`;
let mutation = "updatePerson";
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
} else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
url = `/api/1.0/thirdparty/thirdparty/${id}.json`;
mutation = 'updateThirdparty'
}
makeFetch('PATCH', url, body)
.then((response) => {
commit(mutation, {target: payload.target, thirdparty: response});
})
.catch((error) => {
throw error;
})
},
updateDocumentTitle({commit}, payload) {
commit('updateDocumentTitle', payload)
}
}
});

View File

@@ -57,13 +57,17 @@
v-bind:item="item"
v-bind:search="search"
v-bind:type="checkUniq"
@saveFormOnTheFly="saveFormOnTheFly"
@newPriorSuggestion="newPriorSuggestion"
@updateSelected="updateSelected">
</person-suggestion>
<div class="create-button">
<on-the-fly
v-if="query.length >= 3"
v-if="queryLength >= 3 && (options.type.includes('person') || options.type.includes('thirdparty'))"
:buttonText="$t('onthefly.create.button', {q: query})"
:allowedTypes="options.type"
:query="query"
action="create"
@saveFormOnTheFly="saveFormOnTheFly"
ref="onTheFly">
@@ -247,7 +251,7 @@ export default {
}
},
newPriorSuggestion(entity) {
console.log('newPriorSuggestion', entity);
// console.log('newPriorSuggestion', entity);
if (entity !== null) {
let suggestion = {
key: entity.type + entity.id,

View File

@@ -18,6 +18,7 @@
<suggestion-third-party
v-if="item.result.type === 'thirdparty'"
@newPriorSuggestion="newPriorSuggestion"
v-bind:item="item">
</suggestion-third-party>
@@ -26,10 +27,10 @@
v-bind:item="item">
</suggestion-user>
<suggestion-household
v-if="item.result.type === 'household'"
v-bind:item="item">
</suggestion-household>
<suggestion-household
v-if="item.result.type === 'household'"
v-bind:item="item">
</suggestion-household>
</label>
</div>
@@ -54,7 +55,7 @@ export default {
'search',
'type'
],
emits: ['updateSelected'],
emits: ['updateSelected', 'newPriorSuggestion'],
computed: {
selected: {
set(value) {
@@ -72,6 +73,9 @@ export default {
methods: {
setValueByType(value, type) {
return (type === 'radio')? [value] : value;
},
newPriorSuggestion(response) {
this.$emit('newPriorSuggestion', response)
}
}
};

View File

@@ -1,13 +1,14 @@
<template>
<div class="container tpartycontainer">
<div class="tparty-identification">
<span v-if="item.result.profession" class="profession">{{ item.result.profession.name.fr }}</span>
<span class="name">
{{ item.result.text }}&nbsp;
{{ item.result.text }}&nbsp;
</span>
<span class="location">
<template v-if="hasAddress">
{{ getAddress.text }} -
{{ getAddress.postcode.name }}
{{ getAddress.text }} -
{{ getAddress.postcode.name }}
</template>
</span>
</div>
@@ -19,21 +20,28 @@
</div>
<div class="right_actions">
<badge-entity
:entity="item.result"
:options="{ displayLong: true }">
</badge-entity>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
<badge-entity
:entity="item.result"
:options="{ displayLong: true }">
</badge-entity>
<on-the-fly v-if="item.result.kind === 'company'"
v-bind:parent="item.result"
@saveFormOnTheFly="saveFormOnTheFly"
action="addContact"
ref="onTheFly"
></on-the-fly>
<on-the-fly
type="thirdparty"
v-bind:id="item.result.id"
action="show">
</on-the-fly>
</div>
</template>
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = {
messages: {
@@ -48,54 +56,79 @@ const i18n = {
};
export default {
name: 'SuggestionThirdParty',
components: {
OnTheFly,
BadgeEntity
},
props: ['item'],
i18n,
computed: {
hasAddress() {
if (this.$props.item.result.address !== null) {
return true;
}
if (this.$props.item.result.parent !== null) {
this.$props.item.result.parent.address !== null;
}
},
hasParent() {
return this.$props.item.result.parent !== null;
},
getAddress() {
if (this.$props.item.result.address !== null) {
return this.$props.item.result.address;
}
if (this.$props.item.result.parent.address !== null) {
return this.$props.item.result.parent.address;
}
name: 'SuggestionThirdParty',
components: {
OnTheFly,
BadgeEntity
},
props: ['item'],
emits: ['newPriorSuggestion'],
i18n,
computed: {
hasAddress() {
if (this.$props.item.result.address !== null) {
return true;
}
if (this.$props.item.result.parent !== null) {
this.$props.item.result.parent.address !== null;
}
},
hasParent() {
return this.$props.item.result.parent !== null;
},
getAddress() {
if (this.$props.item.result.address !== null) {
return this.$props.item.result.address;
}
if (this.$props.item.result.parent.address !== null) {
return this.$props.item.result.parent.address;
}
return null;
}
}
return null;
}
},
methods: {
saveFormOnTheFly({data, type}) {
makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data)
.then(response => {
this.$emit('newPriorSuggestion', response);
this.$refs.onTheFly.closeModal();
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
}
}
</script>
<style lang="scss" scoped>
.tpartycontainer {
.tpartyparent {
.name {
font-weight: bold;
font-variant: all-small-caps;
.tpartyparent {
.name {
font-weight: bold;
font-variant: all-small-caps;
}
}
.tparty-identification {
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
.tparty-identification {
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
.profession {
font-weight: 800;
color: black;
font-style: normal !important;
}
}
}
</style>

View File

@@ -12,6 +12,7 @@
<!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.suffixText" class="suffixtext">&nbsp;{{ person.suffixText }}</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
<span :class="'altname altname-' + altNameKey">{{ altNameLabel }}</span>
</span>
@@ -20,6 +21,7 @@
<!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.suffixText" class="suffixtext">&nbsp;{{ person.suffixText }}</span>
<span v-if="person.deathdate" class="deathdate"> ()</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
<span :class="'altname altname-' + altNameKey">{{ altNameLabel }}</span>
@@ -85,6 +87,15 @@
</p>
</li>
<li v-if="person.email">
<i class="fa fa-li fa-envelope-o"></i>
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-envelope-o"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + person.mobilenumber">{{ person.mobilenumber }}</a>
@@ -102,6 +113,15 @@
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.email">
<i class="fa fa-li fa-envelope-o"></i>
<a :href="'mailto: ' + person.email">{{ person.email }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-envelope-o"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>

View File

@@ -6,6 +6,7 @@
<span v-if="person.altNames && person.altNames.length > 0" class="altnames">
<span :class="'altname altname-' + altNameKey"> ({{ altNameLabel }})</span>
</span>
<span v-if="person.suffixText" class="suffixtext">&nbsp;{{ person.suffixText }}</span>
<span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $tc('renderbox.years_old', person.age) }}</span>
<span v-else-if="this.addAge && person.deathdate !== null">&nbsp;()</span>
</span>

View File

@@ -32,6 +32,14 @@
<label for="lastname">{{ $t('person.lastname') }}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li v-for="(qi, i) in queryItems" :key="i" @click="addQueryItem('lastName', qi)">
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div class="form-floating mb-3">
<input
class="form-control form-control-lg"
@@ -43,6 +51,14 @@
<label for="firstname">{{ $t('person.firstname') }}</label>
</div>
<div v-if="queryItems">
<ul class="list-suggest add-items inline">
<li v-for="(qi, i) in queryItems" :key="i" @click="addQueryItem('firstName', qi)">
<span class="person-text">{{ qi }}</span>
</li>
</ul>
</div>
<div v-for="(a, i) in config.altNames" :key="a.key" class="form-floating mb-3">
<input
class="form-control form-control-lg"
@@ -122,7 +138,7 @@ import PersonRenderBox from '../Entity/PersonRenderBox.vue';
export default {
name: "OnTheFlyPerson",
props: ['id', 'type', 'action'],
props: ['id', 'type', 'action', 'query'],
//emits: ['createAction'],
components: {
PersonRenderBox
@@ -203,6 +219,9 @@ export default {
},
personAltNamesLabels() {
return this.person.altNames.map(a => a ? a.label : '');
},
queryItems() {
return this.query ? this.query.split(' ') : null;
}
},
mounted() {
@@ -244,6 +263,16 @@ export default {
)
this.person.altNames = updateAltNames;
},
addQueryItem(field, queryItem) {
switch (field) {
case 'lastName':
this.person.lastName = queryItem;
break;
case 'firstName':
this.person.firstName = queryItem;
break;
}
}
}
}
</script>

View File

@@ -101,14 +101,31 @@
{% if notif_counter.total > 0 %}
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) }}
{% endif %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %}
{{ macro.updatedBy(w) }}
</div>
{% if displayAction is defined and displayAction == true %}
<div class="item-col">
<ul class="record_actions">
{% set suppEvaluations = [] %}
{% for e in w.accompanyingPeriodWorkEvaluations %}
{% set suppEvaluations = suppEvaluations|merge([
{'relatedEntityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation', 'relatedEntityId': e.id }
]) %}
{% for d in e.documents %}
{% set suppEvaluations = suppEvaluations|merge([
{'relatedEntityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluationDocument', 'relatedEntityId': d.id }
]) %}
{% endfor %}
{% endfor %}
<li>
{{ chill_entity_workflow_list(
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork',
w.id, [], suppEvaluations) }}
</li>
<li>
<a class="btn btn-edit" title="{{ 'Edit'|trans }}"
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"

View File

@@ -2,6 +2,16 @@
{% block title 'accompanying_course_work.List accompanying course work'|trans %}
{% block js %}
{{ parent() }}
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
{% endblock %}
{% block content %}
<div class="accompanying_course_work">

View File

@@ -0,0 +1,22 @@
{#
content of the notification if the person move and the person "localize" the period
#}{{ period.user.label }},
L'usager {{ oldPersonLocation|chill_entity_render_string }} a déménagé.
Son adresse était utilisée pour localiser le parcours n°{{ period.id }}, dont vous êtes
le référent.
En conséquence de ce déménage, le parcours est toujours localisé à cette adresse, mais à l'aide d'une
adresse temporaire.
Si vous continuez à suivre le parcours, vous pouvez le localiser à nouveau auprès de l'adresse de
l'usager {{ oldPersonLocation|chill_entity_render_string }}.
Sinon, veillez à vous assurer de la continuité du suivi par vos collègues.
Pour visualiser le parcours, cliquez ici:
{{ absolute_url(path('chill_person_accompanying_course_index', {'accompanying_period_id': period.id})) }}
Cordialement,

View File

@@ -5,6 +5,12 @@
{% block title %}{{ 'My accompanying periods in draft'|trans }}{% endblock title %}
{% macro recordAction(period) %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_DELETE', period) %}
<li>
<a href="{{ path('chill_person_accompanying_course_delete', {'accompanying_period_id': period.id }) }}"
class="btn btn-delete" title="{{ 'Delete'|trans }}"></a>
</li>
{% endif %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period.id }) }}"
class="btn btn-show" title="{{ 'See accompanying period'|trans }}"></a>

View File

@@ -12,6 +12,7 @@
* hLevel integer
* addDeath bool
* addAgeBadge bool
* suffixText bool
* address_multiline bool
* customButtons [
'before' Twig\Markup, (injected with macro)
@@ -46,6 +47,11 @@
<span>({{- 'years_old'|trans({ 'age': person.age }) -}})</span>
{% endif %}
{% endif %}
{%- if options['suffixText'] is defined -%}
{% for o in options['suffixText'] %}
<span>{{ o }}</span>
{% endfor %}
{% endif %}
{% endmacro raw %}
{% macro label(person, options) %}
@@ -172,7 +178,7 @@
{%- if options['customButtons']['replace'] is defined -%}
{{ options['customButtons']['replace'] }}
{%- elseif is_granted('CHILL_PERSON_SEE', person) -%}
{%- elseif is_granted('CHILL_PERSON_SEE', person) and options['addLink'] -%}
<li>
<a class="btn btn-sm btn-show" title="{{ 'Show person'|trans }}"
href="{{ path('chill_person_view', { person_id: person.id }) }}"></a>

View File

@@ -1,12 +1,41 @@
{% macro updatedBy(entity) %}
<div class="updatedBy">
{{ 'Last updated on'|trans }}
<span class="date">
{{ entity.updatedAt|format_datetime('medium', 'short') }}
</span>,
{{ 'by_user'|trans }}
<span class="user">
{{ entity.updatedBy|chill_entity_render_box }}
</span>
{% if entity.updatedAt != null %}
{{ 'Last updated on'|trans }}
<span class="date">
{{ entity.updatedAt|format_datetime('medium', 'short') }}
</span>
{% endif %}
{% if entity.updatedBy != null %}
{% if entity.updatedAt != null %}
{{ ', ' ~ 'by_user'|trans }}
{% else %}
{{ 'Last updated by'|trans }}
{% endif %}
<span class="user">
{{ entity.updatedBy|chill_entity_render_box }}
</span>
{% endif %}
</div>
{% endmacro %}
{% macro createdBy(entity) %}
<div class="createdBy">
{% if entity.createdAt != null %}
{{ 'Created on'|trans }}
<span class="date">
{{ entity.createdAt|format_datetime('medium', 'short') }}
</span>
{% endif %}
{% if entity.createdBy != null %}
{% if entity.createdAt != null %}
{{ ', ' ~ 'by_user'|trans }}
{% else %}
{{ 'Created by'|trans }}
{% endif %}
<span class="user">
{{ entity.createdBy|chill_entity_render_string }}
</span>
{% endif %}
</div>
{% endmacro %}

View File

@@ -56,6 +56,11 @@
{% if action is defined %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ path('chill_person_resource_list', { 'person_id': resource.personOwner.id } ) }}" class="btn btn-cancel">
{{ 'Cancel'|trans }}
</a>
</li>
<li class="edit">
<button class="btn btn-edit"
type="submit" id="newPersonResource">

View File

@@ -22,41 +22,33 @@
<div class="flex-table">
{% for resource in personResources %}
<div class="item-bloc">
<div class="item-row">
<div class="item-col" style="width: 50%">
{% if resource.kind is not null %}
<div class="item-row">
<section>
<p>{{ resource.kind.title.fr|capitalize }}</p>
</section>
</div>
{% endif %}
<div class="{% if resource.kind is not null %} separator {% endif %}">
{% if resource.person is not null %}
<div class="denomination h3">
<span>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: resource.person.id },
buttonText: resource.person|chill_entity_render_string,
isDead: resource.person.deathdate is not null
} %}
</span>
</div>
{{ resource.person|chill_entity_render_box({
'render': 'bloc',
'addLink': true,
'addInfo': true,
'addAge': true
}) }}
{% elseif resource.thirdparty is not null %}
<div class="denomination h3">
<span>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'thirdparty', id: resource.thirdparty.id },
buttonText: resource.thirdParty|chill_entity_render_string,
parent: resource.thirdparty.parent
} %}
</span>
</div>
{{ resource.thirdparty|chill_entity_render_box({
'render': 'bloc',
'showContacts': false,
'addLink': true,
'isConfidential': (resource.thirdparty.contactDataAnonymous ? true : false)
}) }}
{% else %}
<div class="denomination h3">
<span>{{ resource.freetext }}</span>
</div>
{% endif %}
</div>
<div class="item-col" style="justify-content: flex-end; ">
{% 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">

View File

@@ -1,19 +0,0 @@
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as m %}
<div class="flex-grow-1 {% if add_classes is defined %}{{ add_classes }}{% else %}h2{% endif %}">
<div>
{% if concerne is defined and concerne == true %}
<span class="item-key">{{ 'Concerne'|trans }}: </span>
{% endif %}
{{ 'workflow.Work (n°%w%)'|trans({'%w%': work.id }) }}
{% if description is defined and description == true %}
{{ ' — ' ~ work.socialAction|chill_entity_render_string }}
{% endif %}
</div>
{% if breadcrumb is defined and breadcrumb == true %}
{{ m.breadcrumb(_context) }}
{% endif %}
</div>

View File

@@ -62,10 +62,14 @@
</li>
{% endif %}
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>
<b>{{ evaluation.createdAt|format_date('short') }}</b>
{% endif %}
</li>
</ul>
{% if evaluation.comment %}

View File

@@ -1,19 +0,0 @@
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as m %}
<div class="flex-grow-1 {% if add_classes is defined %}{{ add_classes }}{% else %}h2{% endif %}">
<div>
{% if concerne is defined and concerne == true %}
<span class="item-key">{{ 'Concerne'|trans }}: </span>
{% endif %}
{{ 'workflow.Evaluation (n°%eval%)'|trans({'%eval%': evaluation.id}) }}
{% if description is defined and description == true %}
{{ ' — ' ~ evaluation.evaluation.title|localize_translatable_string }}
{% endif %}
</div>
{% if breadcrumb is defined and breadcrumb == true %}
{{ m.breadcrumb(_context) }}
{% endif %}
</div>

View File

@@ -0,0 +1,103 @@
<div class="flex-table accompanying_course_work-list">
<div class="item-bloc evaluation-item bg-chill-llight-gray">
<div class="item-row mb-2">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">
{{ evaluation.accompanyingPeriodWork.socialAction|chill_entity_render_string }}
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.accompanyingPeriodWork.startDate|format_date('short') }}</b>
</li>
{% if evaluation.accompanyingPeriodWork.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.accompanyingPeriodWork.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
</span>
</h2>
</div>
<div class="item-row column">
<table class="obj-res-eval my-3" style="font-size: 110% !important;">
<thead>
<tr>
<th class="eval">
<h4 class="title_label">
{{ 'Évaluation'|trans }}
</h4>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="eval">
<ul class="eval_title">
<li class="my-2">
{{ evaluation.evaluation.title|localize_translatable_string }}
<ul class="columns pt-2">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.startDate|format_date('short') }}</b>
</li>
{% if evaluation.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.endDate|format_date('short') }}</b>
</li>
{% endif %}
{% if evaluation.maxDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.max_date'|trans ~ ' : ' }}</span>
<b>{{ evaluation.maxDate|format_date('short') }}</b>
</li>
{% endif %}
{% if evaluation.warningInterval and evaluation.warningInterval.d > 0 %}
<li>
{% set days = (evaluation.warningInterval.d + evaluation.warningInterval.m * 30) %}
<span class="item-key">{{ 'accompanying_course_work.warning_interval'|trans ~ ' : ' }}</span>
{{ 'accompanying_course_work.%days% days before max_date'|trans({'%days%': days }) }}
</li>
{% endif %}
<li>
{% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b>
{% endif %}
{% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span>
<b>{{ evaluation.createdAt|format_date('short') }}</b>
{% endif %}
</li>
</ul>
{% if evaluation.comment %}
<blockquote class="chill-user-quote" style="margin-left: 0;">
{{ evaluation.comment }}
</blockquote>
{% endif %}
</li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<div class="item-row">
{% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %}
{{ macro.updatedBy(evaluation) }}
</div>
</div>
</div>
{% if display_action is defined and display_action == true %}
{# TODO add acl #}
<ul class="record_actions">
<li>
<a class="btn btn-show" href="{{ path('chill_person_accompanying_period_work_edit', {'id': evaluation.accompanyingPeriodWork.id}) }}">
{{ 'Show'|trans }}
</a>
</li>
</ul>
{% endif %}

View File

@@ -15,7 +15,7 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonNotDuplicateRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
@@ -39,7 +39,7 @@ class SimilarPersonMatcher
protected PersonNotDuplicateRepository $personNotDuplicateRepository;
protected PersonRender $personRender;
protected PersonRenderInterface $personRender;
/**
* @var TokenStorageInterface
@@ -51,7 +51,7 @@ class SimilarPersonMatcher
AuthorizationHelper $authorizationHelper,
TokenStorageInterface $tokenStorage,
PersonNotDuplicateRepository $personNotDuplicateRepository,
PersonRender $personRender
PersonRenderInterface $personRender
) {
$this->em = $em;
$this->authorizationHelper = $authorizationHelper;

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\Security\Authorization;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use UnexpectedValueException;
/**
* Voter for AccompanyingPeriodWorkEvaluationDocument.
*
* Delegates to the sames authorization than for Evalution
*/
class AccompanyingPeriodWorkEvaluationDocumentVoter extends Voter
{
public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_EVALUATION_DOCUMENT_SHOW';
private AccessDecisionManagerInterface $accessDecisionManager;
public function __construct(AccessDecisionManagerInterface $accessDecisionManager)
{
$this->accessDecisionManager = $accessDecisionManager;
}
protected function supports($attribute, $subject)
{
return $subject instanceof AccompanyingPeriodWorkEvaluationDocument
&& self::SEE === $attribute;
}
/**
* @param string $attribute
* @param AccompanyingPeriodWorkEvaluationDocument $subject
*
* @return bool|void
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
switch ($attribute) {
case self::SEE:
return $this->accessDecisionManager->decide(
$token,
[AccompanyingPeriodWorkEvaluationVoter::SEE],
$subject->getAccompanyingPeriodWorkEvaluation()
);
default:
throw new UnexpectedValueException("The attribute {$attribute} is not supported");
}
}
}

View File

@@ -0,0 +1,124 @@
<?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\Serializer\Normalizer;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use function array_key_exists;
use function array_merge;
use function is_array;
/**
* This denormalizer rely on AbstractNormalizer for most of the job, and
* add some logic for synchronizing collection.
*/
class AccompanyingPeriodWorkEvaluationDenormalizer implements ContextAwareDenormalizerInterface, DenormalizerAwareInterface
{
use DenormalizerAwareTrait;
use ObjectToPopulateTrait;
private EntityManagerInterface $em;
private AccompanyingPeriodWorkRepository $workRepository;
public function __construct(
AccompanyingPeriodWorkRepository $workRepository,
EntityManagerInterface $em
) {
$this->workRepository = $workRepository;
$this->em = $em;
}
public function denormalize($data, $type, $format = null, array $context = [])
{
$evaluation = $this->denormalizer->denormalize($data, $type, $format, array_merge(
$context,
['skip' => self::class]
));
$this->handleDocumentCollection($data, $evaluation, $format, $context);
return $evaluation;
}
public function supportsDenormalization($data, $type, $format = null, array $context = []): bool
{
return AccompanyingPeriodWorkEvaluation::class === $type
&& self::class !== ($context['skip'] ?? null)
&& is_array($data)
&& array_key_exists('type', $data)
&& 'accompanying_period_work_evaluation' === $data['type'];
}
private function handleDocumentCollection(array $data, AccompanyingPeriodWorkEvaluation $evaluation, string $format, array $context)
{
$dataById = [];
$dataWithoutId = [];
foreach ($data['documents'] as $e) {
if (array_key_exists('id', $e)) {
$dataById[$e['id']] = $e;
} else {
$dataWithoutId[] = $e;
}
}
//partition the separate kept documents and removed one
[$kept, $removed] = $evaluation->getDocuments()
->partition(
static fn (int $key, AccompanyingPeriodWorkEvaluationDocument $a) => array_key_exists($a->getId(), $dataById)
);
// remove the document from evaluation
foreach ($removed as $r) {
$evaluation->removeDocument($r);
}
// handle the documents kept
foreach ($kept as $k) {
$this->denormalizer->denormalize(
$dataById[$k->getId()],
AccompanyingPeriodWorkEvaluationDocument::class,
$format,
array_merge(
$context,
[
'groups' => ['write'],
AbstractNormalizer::OBJECT_TO_POPULATE => $k,
]
)
);
}
// create new document
foreach ($dataWithoutId as $newData) {
$document = $this->denormalizer->denormalize(
$newData,
AccompanyingPeriodWorkEvaluationDocument::class,
$format,
array_merge(
$context,
['groups' => ['accompanying_period_work_evaluation:create']]
)
);
$evaluation->addDocument($document);
}
}
}

View File

@@ -0,0 +1,71 @@
<?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\Serializer\Normalizer;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Workflow\Registry;
use function array_key_exists;
class AccompanyingPeriodWorkEvaluationDocumentNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
{
use NormalizerAwareTrait;
private const SKIP = 'accompanying_period_work_evaluation_document_skip';
private EntityWorkflowRepository $entityWorkflowRepository;
private MetadataExtractor $metadataExtractor;
private Registry $registry;
public function __construct(EntityWorkflowRepository $entityWorkflowRepository, MetadataExtractor $metadataExtractor, Registry $registry)
{
$this->entityWorkflowRepository = $entityWorkflowRepository;
$this->metadataExtractor = $metadataExtractor;
$this->registry = $registry;
}
public function normalize($object, ?string $format = null, array $context = []): array
{
$initial = $this->normalizer->normalize($object, $format, array_merge($context, [
self::SKIP => spl_object_hash($object),
]));
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluationDocument::class,
$object->getId()
);
$workflows = $this->entityWorkflowRepository->findBy([
'relatedEntityClass' => AccompanyingPeriodWorkEvaluationDocument::class,
'relatedEntityId' => $object->getId(),
]);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context);
return $initial;
}
public function supportsNormalization($data, ?string $format = null, array $context = [])
{
return $data instanceof AccompanyingPeriodWorkEvaluationDocument
&& 'json' === $format
&& (
!array_key_exists(self::SKIP, $context)
|| spl_object_hash($data) !== $context[self::SKIP]
);
}
}

View File

@@ -49,6 +49,15 @@ class AccompanyingPeriodWorkEvaluationNormalizer implements ContextAwareNormaliz
[self::IGNORE_EVALUATION => spl_object_hash($object)]
));
// due to bug: https://api-platform.com/docs/core/serialization/#collection-relation
// and also: https://github.com/symfony/symfony/issues/36965
// we have to rewrite the documents as a collection
$initial['documents'] = $this->normalizer->normalize(
$object->getDocuments()->getValues(),
$format,
$context
);
// then, we add normalization for things which are not into the entity
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(

View File

@@ -16,6 +16,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
@@ -56,6 +57,15 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
[self::IGNORE_WORK => spl_object_hash($object)]
));
// due to bug: https://api-platform.com/docs/core/serialization/#collection-relation
// and also: https://github.com/symfony/symfony/issues/36965
// we have to rewrite the evaluations as a collection
$initial['accompanyingPeriodWorkEvaluations'] = $this->normalizer->normalize(
$object->getAccompanyingPeriodWorkEvaluations()->getValues(),
$format,
$context
);
// then, we add normalization for things which are not into the entity
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(
@@ -67,10 +77,15 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
AccompanyingPeriodWorkEvaluation::class
);
$initial['workflows_availables_evaluation_documents'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluationDocument::class
);
$workflows = $this->entityWorkflowRepository->findBy([
'relatedEntityClass' => AccompanyingPeriodWork::class,
'relatedEntityId' => $object->getId(),
]);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context);
return $initial;

View File

@@ -19,7 +19,7 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonAltName;
use Chill\PersonBundle\Repository\Relationships\RelationshipRepository;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
@@ -37,7 +37,7 @@ class PersonDocGenNormalizer implements
{
use NormalizerAwareTrait;
private PersonRender $personRender;
private PersonRenderInterface $personRender;
private RelationshipRepository $relationshipRepository;
@@ -46,7 +46,7 @@ class PersonDocGenNormalizer implements
private TranslatorInterface $translator;
public function __construct(
PersonRender $personRender,
PersonRenderInterface $personRender,
RelationshipRepository $relationshipRepository,
TranslatorInterface $translator,
TranslatableStringHelper $translatableStringHelper

View File

@@ -23,21 +23,15 @@ use Doctrine\Common\Collections\Collection;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use function array_key_exists;
/**
* Serialize a Person entity.
*/
class PersonJsonNormalizer implements
DenormalizerAwareInterface,
DenormalizerInterface,
NormalizerAwareInterface,
NormalizerInterface
class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwareInterface, PersonJsonNormalizerInterface
{
use DenormalizerAwareTrait;

View File

@@ -0,0 +1,24 @@
<?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\Serializer\Normalizer;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
/**
* Serialize a Person entity.
*/
interface PersonJsonNormalizerInterface extends
DenormalizerInterface,
NormalizerInterface
{
}

View File

@@ -23,7 +23,7 @@ use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
@@ -48,7 +48,7 @@ class AccompanyingPeriodContext implements
private NormalizerInterface $normalizer;
private PersonRender $personRender;
private PersonRenderInterface $personRender;
private TranslatableStringHelperInterface $translatableStringHelper;
@@ -59,7 +59,7 @@ class AccompanyingPeriodContext implements
NormalizerInterface $normalizer,
TranslatableStringHelperInterface $translatableStringHelper,
EntityManagerInterface $em,
PersonRender $personRender,
PersonRenderInterface $personRender,
TranslatorInterface $translator,
BaseContextData $baseContextData
) {

View File

@@ -22,7 +22,7 @@ use function array_key_exists;
/**
* Render a Person.
*/
class PersonRender extends AbstractChillEntityRender
class PersonRender extends AbstractChillEntityRender implements PersonRenderInterface
{
private ConfigPersonAltNamesHelper $configAltNamesHelper;
@@ -59,6 +59,7 @@ class PersonRender extends AbstractChillEntityRender
'customArea' => $options['customArea'] ?? [],
'addDeath' => $options['addDeath'] ?? true,
'addAgeBadge' => $options['addAgeBadge'] ?? false,
'suffixText' => $options['suffixText'] ?? [],
];
return

View File

@@ -0,0 +1,21 @@
<?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;
/**
* Render a Person.
*/
interface PersonRenderInterface extends ChillEntityRenderInterface
{
}

View File

@@ -0,0 +1,290 @@
<?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 AccompanyingPeriod\Events;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\AccompanyingPeriod\Events\PersonAddressMoveEventSubscriber;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use DateTime;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use ReflectionClass;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* @internal
* @coversNothing
*/
final class PersonMoveEventSubscriberTest extends KernelTestCase
{
use ProphecyTrait;
public function testEventChangeHouseholdNoNotificationForPeriodWithoutUser()
{
$person = new Person();
$period = new AccompanyingPeriod();
$period
->setStep(AccompanyingPeriod::STEP_CONFIRMED)
->setPersonLocation($person)
->addPerson($person);
$this->forceIdToPeriod($period);
$previousHousehold = (new Household())->addAddress(
(new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$nextHousehold = (new Household())->addAddress(
(new Address())->setValidFrom(new DateTime('tomorrow'))
);
$nextMembership = new HouseholdMember();
$nextMembership
->setPerson($person)
->setHousehold($nextHousehold)
->setStartDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership)
->setNextMembership($nextMembership);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldNotBeCalled();
$eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null);
$eventSubscriber->resetPeriodLocation($event);
}
public function testEventChangeHouseholdNotification()
{
$person = new Person();
$period = new AccompanyingPeriod();
$period
->setStep(AccompanyingPeriod::STEP_CONFIRMED)
->setPersonLocation($person)
->addPerson($person)
->setUser(new User());
$this->forceIdToPeriod($period);
$previousHousehold = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$nextHousehold = (new Household())->addAddress(
(new Address())->setValidFrom(new DateTime('tomorrow'))
);
$nextMembership = new HouseholdMember();
$nextMembership
->setPerson($person)
->setHousehold($nextHousehold)
->setStartDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership)
->setNextMembership($nextMembership);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
$eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null);
$eventSubscriber->resetPeriodLocation($event);
$this->assertNotNull($period->getAddressLocation());
$this->assertNull($period->getPersonLocation());
}
public function testEventChangeHouseholdNotificationForPeriodChangeLocationOfPersonAnteriorToCurrentLocationHistory()
{
$person = new Person();
$period = new AccompanyingPeriod();
$period
->setStep(AccompanyingPeriod::STEP_CONFIRMED)
->setPersonLocation($person)
->setUser(new User())
->addPerson($person);
$this->forceIdToPeriod($period);
$previousHousehold = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$nextHousehold = (new Household())->addAddress(
(new Address())->setValidFrom(new DateTime('1 month ago'))
);
$nextMembership = new HouseholdMember();
$nextMembership
->setPerson($person)
->setHousehold($nextHousehold)
->setStartDate(new DateTimeImmutable('1 month ago'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership)
->setNextMembership($nextMembership);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalled(1);
$eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null);
$eventSubscriber->resetPeriodLocation($event);
$this->assertNotNull($period->getAddressLocation());
$this->assertNull($period->getPersonLocation());
}
public function testEventLeaveNotification()
{
$person = new Person();
$period = new AccompanyingPeriod();
$period
->setStep(AccompanyingPeriod::STEP_CONFIRMED)
->setPersonLocation($person)
->addPerson($person)
->setUser(new User());
$this->forceIdToPeriod($period);
$previousHousehold = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
$eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null);
$eventSubscriber->resetPeriodLocation($event);
$this->assertNotNull($period->getAddressLocation());
$this->assertNull($period->getPersonLocation());
}
public function testEventPersonChangeAddressInThePast()
{
$person = new Person();
$period = new AccompanyingPeriod();
$period
->setStep(AccompanyingPeriod::STEP_CONFIRMED)
->setPersonLocation($person)
->addPerson($person)
->setUser(new User());
$this->forceIdToPeriod($period);
$membership = new HouseholdMember();
$membership
->setPerson($person)
->setHousehold($household = new Household())
->setStartDate(new DateTimeImmutable('1 year ago'));
$previousAddress = new Address();
$previousAddress->setValidFrom(new DateTime('6 months ago'));
$household->addAddress($previousAddress);
$newAddress = new Address();
$newAddress->setValidFrom(new DateTime('tomorrow'));
$household->addAddress($newAddress);
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousAddress($household->getPreviousAddressOf($newAddress))
->setNextAddress($newAddress);
$em = $this->prophesize(EntityManagerInterface::class);
$em->persist(Argument::type(Notification::class))->shouldBeCalledTimes(1);
$eventSubscriber = $this->buildSubscriber(null, $em->reveal(), null, null);
$eventSubscriber->resetPeriodLocation($event);
$this->assertNotNull($period->getAddressLocation());
$this->assertNull($period->getPersonLocation());
}
private function buildSubscriber(
?EngineInterface $engine = null,
?EntityManagerInterface $entityManager = null,
?Security $security = null,
?TranslatorInterface $translator = null
): PersonAddressMoveEventSubscriber {
if (null === $translator) {
$double = $this->prophesize(TranslatorInterface::class);
$translator = $double->reveal();
}
if (null === $security) {
$double = $this->prophesize(Security::class);
$double->getUser()->willReturn(new User());
$security = $double->reveal();
}
if (null === $engine) {
$double = $this->prophesize(EngineInterface::class);
$engine = $double->reveal();
}
if (null === $entityManager) {
$double = $this->prophesize(EntityManagerInterface::class);
$entityManager = $double->reveal();
}
return new PersonAddressMoveEventSubscriber(
$engine,
$entityManager,
$security,
$translator
);
}
private function forceIdToPeriod(AccompanyingPeriod $period): void
{
$reflectionClass = new ReflectionClass($period);
$property = $reflectionClass->getProperty('id');
$property->setAccessible(true);
$property->setValue($period, 0);
}
}

View File

@@ -11,6 +11,8 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Tests\Entity;
use ArrayIterator;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
@@ -60,6 +62,62 @@ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$this->assertFalse($period->isClosingAfterOpening());
}
public function testHistoryLocation()
{
$period = new AccompanyingPeriod();
$person = new Person();
$address = new Address();
$period->setPersonLocation($person);
$this->assertCount(0, $period->getLocationHistories());
$period->setAddressLocation($address);
$period->setPersonLocation(null);
$this->assertCount(0, $period->getLocationHistories());
$period->setStep(AccompanyingPeriod::STEP_CONFIRMED);
$this->assertCount(1, $period->getLocationHistories());
$this->assertSame($address, $period->getLocationHistories()->first()->getAddressLocation());
$period->setPersonLocation($person);
$period->setAddressLocation(null);
$this->assertCount(2, $period->getLocationHistories());
$this->assertSame($person, $period->getLocationHistories()->last()->getPersonLocation());
$period->setAddressLocation($address);
$period->setPersonLocation(null);
$this->assertCount(3, $period->getLocationHistories());
$locations = $period->getLocationHistories()->toArray();
usort($locations, static function (AccompanyingPeriod\AccompanyingPeriodLocationHistory $a, AccompanyingPeriod\AccompanyingPeriodLocationHistory $b) {
return $a->getStartDate() <=> $b->getStartDate();
});
$iterator = new ArrayIterator($locations);
$iterator->rewind();
do {
$current = $iterator->current();
$iterator->next();
if ($iterator->valid()) {
$next = $iterator->current();
$this->assertNotNull($current->getEndDate());
$this->assertEquals($current->getEndDate(), $next->getStartDate());
} else {
$this->assertNull($current->getEndDate());
}
} while ($iterator->valid());
}
public function testIsClosed()
{
$period = new AccompanyingPeriod(new DateTime());

View File

@@ -11,8 +11,12 @@ declare(strict_types=1);
namespace Entity\Household;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdComposition;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
@@ -22,6 +26,90 @@ use PHPUnit\Framework\TestCase;
*/
final class HouseholdTest extends TestCase
{
public function testGetMembersOnRange()
{
$household = new Household();
$household->addMember($householdMemberA = (new HouseholdMember())
->setStartDate(new DateTimeImmutable('2020-01-01'))
->setEndDate(new DateTimeImmutable('2020-12-31'))
->setPerson(new Person()));
$household->addMember($householdMemberB = (new HouseholdMember())
->setStartDate(new DateTimeImmutable('2020-06-01'))
->setEndDate(new DateTimeImmutable('2021-06-31'))
->setPerson(new Person()));
$household->addMember($householdMemberC = (new HouseholdMember())
->setStartDate(new DateTimeImmutable('2021-01-01'))
->setEndDate(null)
->setPerson(new Person()));
$members = $household->getMembersOnRange(new DateTimeImmutable('2019-01-01'), null);
$this->assertCount(3, $members);
$this->assertContains($householdMemberA, $members);
$this->assertContains($householdMemberB, $members);
$this->assertContains($householdMemberC, $members);
$members = $household->getMembersOnRange(new DateTimeImmutable('2020-01-01'), new DateTimeImmutable('2020-07-01'));
$this->assertCount(2, $members);
$this->assertContains($householdMemberA, $members);
$this->assertContains($householdMemberB, $members);
$this->assertNotContains($householdMemberC, $members);
$members = $household->getMembersOnRange(new DateTimeImmutable('2020-01-01'), new DateTimeImmutable('2022-12-31'));
$this->assertCount(3, $members);
$this->assertContains($householdMemberA, $members);
$this->assertContains($householdMemberB, $members);
$this->assertContains($householdMemberC, $members);
$members = $household->getMembersOnRange(new DateTimeImmutable('2021-01-01'), new DateTimeImmutable('2022-12-31'));
$this->assertCount(2, $members);
$this->assertNotContains($householdMemberA, $members);
$this->assertContains($householdMemberB, $members);
$this->assertContains($householdMemberC, $members);
}
public function testHouseholdAddressConsistent()
{
$household = new Household();
$lastAddress = new Address();
$lastAddress->setValidFrom($yesterday = new DateTime('yesterday'));
$household->addAddress($lastAddress);
$this->assertNull($lastAddress->getValidTo());
$this->assertEquals($yesterday, $lastAddress->getValidFrom());
$previousAddress = new Address();
$previousAddress->setValidFrom($oneMonthAgo = new DateTime('1 month ago'));
$household->addAddress($previousAddress);
$addresses = $household->getAddressesOrdered();
$this->assertSame($previousAddress, $addresses[0]);
$this->assertSame($lastAddress, $addresses[1]);
$this->assertEquals($oneMonthAgo, $previousAddress->getValidFrom());
$this->assertEquals($yesterday, $previousAddress->getValidTo());
$this->assertEquals($yesterday, $lastAddress->getValidFrom());
$this->assertNull($lastAddress->getValidTo());
$futureAddress = new Address();
$futureAddress->setValidFrom($tomorrow = new DateTime('tomorrow'));
$household->addAddress($futureAddress);
$addresses = $household->getAddressesOrdered();
$this->assertSame($previousAddress, $addresses[0]);
$this->assertSame($lastAddress, $addresses[1]);
$this->assertSame($futureAddress, $addresses[2]);
$this->assertEquals($oneMonthAgo, $previousAddress->getValidFrom());
$this->assertEquals($yesterday, $previousAddress->getValidTo());
$this->assertEquals($yesterday, $lastAddress->getValidFrom());
$this->assertEquals($tomorrow, $lastAddress->getValidTo());
$this->assertEquals($tomorrow, $futureAddress->getValidFrom());
$this->assertNull($futureAddress->getValidTo());
}
public function testHouseholdComposition()
{
$household = new Household();

View File

@@ -0,0 +1,118 @@
<?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 Event\Person;
use Chill\MainBundle\Entity\Address;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use DateTime;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
/**
* @internal
* @coversNothing
*/
final class PersonAddressMoveEventTest extends TestCase
{
public function testPersonChangeAddress()
{
$person = new Person();
$household = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$household->addAddress(
($nextAddress = new Address())->setValidFrom(new DateTime('1 month ago'))
);
$member = new HouseholdMember();
$member
->setPerson($person)
->setHousehold($household)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousAddress($previousAddress)
->setNextAddress($nextAddress);
$this->assertSame($previousAddress, $event->getPreviousAddress());
$this->assertSame($nextAddress, $event->getNextAddress());
$this->assertEquals((new DateTime('1 month ago'))->format('Y-m-d'), $nextAddress->getValidFrom()->format('Y-m-d'));
$this->assertEquals((new DateTime('1 month ago'))->format('Y-m-d'), $event->getMoveDate()->format('Y-m-d'));
}
public function testPersonChangeHousehold()
{
$person = new Person();
$previousHousehold = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$nextHousehold = (new Household())->addAddress(
($nextAddress = new Address())->setValidFrom(new DateTime('tomorrow'))
);
$nextMembership = new HouseholdMember();
$nextMembership
->setPerson($person)
->setHousehold($nextHousehold)
->setStartDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership)
->setNextMembership($nextMembership);
$this->assertTrue($event->personChangeHousehold());
$this->assertSame($previousAddress, $event->getPreviousAddress());
$this->assertSame($nextAddress, $event->getNextAddress());
$this->assertTrue($event->personChangeAddress());
$this->assertFalse($event->personLeaveWithoutHousehold());
$this->assertEquals(new DateTimeImmutable('tomorrow'), $event->getMoveDate());
}
public function testPersonLeaveHousehold()
{
$person = new Person();
$previousHousehold = (new Household())->addAddress(
($previousAddress = new Address())->setValidFrom(new DateTime('1 year ago'))
);
$previousMembership = new HouseholdMember();
$previousMembership
->setPerson($person)
->setHousehold($previousHousehold)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('tomorrow'));
$event = new PersonAddressMoveEvent($person);
$event
->setPreviousMembership($previousMembership);
$this->assertTrue($event->personChangeHousehold());
$this->assertSame($previousAddress, $event->getPreviousAddress());
$this->assertNull($event->getNextAddress());
$this->assertTrue($event->personChangeAddress());
$this->assertTrue($event->personLeaveWithoutHousehold());
$this->assertEquals(new DateTimeImmutable('tomorrow'), $event->getMoveDate());
}
}

View File

@@ -14,10 +14,14 @@ namespace Chill\PersonBundle\Tests\Household;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Event\Person\PersonAddressMoveEvent;
use Chill\PersonBundle\Household\MembersEditorFactory;
use DateTimeImmutable;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use function count;
/**
@@ -26,13 +30,13 @@ use function count;
*/
final class MembersEditorTest extends TestCase
{
use ProphecyTrait;
private MembersEditorFactory $factory;
protected function setUp(): void
{
$validator = $this->createMock(ValidatorInterface::class);
$this->factory = new MembersEditorFactory($validator);
$this->factory = $this->buildMembersEditorFactory();
}
public function testMovePersonWithoutSharedHousehold()
@@ -121,4 +125,49 @@ final class MembersEditorTest extends TestCase
);
$this->assertEquals($date, $membership1->getEndDate());
}
public function testPostMove()
{
$person = new Person();
$position = (new Position())
->setShareHousehold(false);
$household1 = new Household();
$household2 = new Household();
$eventDispatcher = $this->prophesize(EventDispatcherInterface::class);
$eventDispatcher
->dispatch(Argument::type(PersonAddressMoveEvent::class))
->shouldBeCalled();
$factory = $this->buildMembersEditorFactory(
$eventDispatcher->reveal(),
null
);
$editor = $factory->createEditor($household1);
$editor->addMovement(new DateTimeImmutable('now'), $person, $position);
$editor->postMove();
}
private function buildMembersEditorFactory(
?EventDispatcherInterface $eventDispatcher = null,
?ValidatorInterface $validator = null
) {
if (null === $eventDispatcher) {
$double = $this->getProphet()->prophesize();
$double->willImplement(EventDispatcherInterface::class);
$double->dispatch(Argument::type(PersonAddressMoveEvent::class));
$eventDispatcher = $double->reveal();
}
if (null === $validator) {
$double = $this->getProphet()->prophesize();
$double->willImplement(ValidatorInterface::class);
$validator = $double->reveal();
}
return new MembersEditorFactory(
$eventDispatcher,
$validator
);
}
}

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -20,9 +20,9 @@ use Symfony\Component\Validator\Exception\UnexpectedValueException;
class LocationValidityValidator extends ConstraintValidator
{
private PersonRender $render;
private PersonRenderInterface $render;
public function __construct(PersonRender $render)
public function __construct(PersonRenderInterface $render)
{
$this->render = $render;
}

View File

@@ -13,7 +13,7 @@ namespace Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod;
use Chill\MainBundle\Util\DateRangeCovering;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Validator\Constraint;
@@ -26,11 +26,11 @@ class ParticipationOverlapValidator extends ConstraintValidator
{
private const MAX_PARTICIPATION = 1;
private PersonRender $personRender;
private PersonRenderInterface $personRender;
private ThirdPartyRender $thirdpartyRender;
public function __construct(PersonRender $personRender, ThirdPartyRender $thirdPartyRender)
public function __construct(PersonRenderInterface $personRender, ThirdPartyRender $thirdPartyRender)
{
$this->personRender = $personRender;
$this->thirdpartyRender = $thirdPartyRender;

View File

@@ -12,7 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Chill\ThirdPartyBundle\Templating\Entity\ThirdPartyRender;
use Doctrine\Common\Collections\Collection;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
@@ -24,11 +24,11 @@ use function in_array;
class ResourceDuplicateCheckValidator extends ConstraintValidator
{
private PersonRender $personRender;
private PersonRenderInterface $personRender;
private ThirdPartyRender $thirdpartyRender;
public function __construct(PersonRender $personRender, ThirdPartyRender $thirdPartyRender)
public function __construct(PersonRenderInterface $personRender, ThirdPartyRender $thirdPartyRender)
{
$this->personRender = $personRender;
$this->thirdpartyRender = $thirdPartyRender;

View File

@@ -13,7 +13,7 @@ namespace Chill\PersonBundle\Validator\Constraints\Household;
use Chill\MainBundle\Util\DateRangeCovering;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRender;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@@ -26,9 +26,9 @@ use function count;
*/
class HouseholdMembershipSequentialValidator extends ConstraintValidator
{
private PersonRender $render;
private PersonRenderInterface $render;
public function __construct(PersonRender $render)
public function __construct(PersonRenderInterface $render)
{
$this->render = $render;
}

View File

@@ -0,0 +1,96 @@
<?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\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocumentRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
{
private AccompanyingPeriodWorkEvaluationDocumentRepository $repository;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(
AccompanyingPeriodWorkEvaluationDocumentRepository $repository,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatorInterface $translator
) {
$this->repository = $repository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$doc = $this->getRelatedEntity($entityWorkflow);
return [
'persons' => $doc->getAccompanyingPeriodWorkEvaluation()
->getAccompanyingPeriodWork()->getPersons(),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
$doc = $this->getRelatedEntity($entityWorkflow);
return $this->translator->trans(
'workflow.Doc for evaluation (n°%eval%)',
['%eval%' => $entityWorkflow->getRelatedEntityId()]
) . ' - ' . $this->translatableStringHelper->localize($doc->getAccompanyingPeriodWorkEvaluation()
->getEvaluation()->getTitle());
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWorkEvaluationDocument
{
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{
return AccompanyingPeriodWorkEvaluationDocumentVoter::SEE;
}
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string
{
return '@ChillPerson/Workflow/_evaluation_document.html.twig';
}
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$doc = $this->getRelatedEntity($entityWorkflow);
return [
'entity_workflow' => $entityWorkflow,
'evaluation' => $doc->getAccompanyingPeriodWorkEvaluation(),
'doc' => $doc,
];
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluationDocument::class;
}
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return false;
}
}

View File

@@ -12,18 +12,48 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowHandlerInterface
{
private AccompanyingPeriodWorkEvaluationRepository $repository;
public function __construct(AccompanyingPeriodWorkEvaluationRepository $repository)
{
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(
AccompanyingPeriodWorkEvaluationRepository $repository,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatorInterface $translator
) {
$this->repository = $repository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$evaluation = $this->getRelatedEntity($entityWorkflow);
return [
'persons' => $evaluation->getAccompanyingPeriodWork()->getPersons(),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
$evaluation = $this->getRelatedEntity($entityWorkflow);
return $this->translator->trans(
'workflow.Evaluation (n°%eval%)',
['%eval%' => $entityWorkflow->getRelatedEntityId()]
) . ' - ' . $this->translatableStringHelper->localize($evaluation->getEvaluation()->getTitle());
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWorkEvaluation
@@ -49,16 +79,6 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
];
}
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
return '@ChillPerson/Workflow/_evaluation.title.html.twig';
}
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array
{
return $this->getTemplateData($entityWorkflow, $options);
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluation::class;

View File

@@ -12,20 +12,48 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Workflow;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface
{
private AccompanyingPeriodWorkRepository $repository;
public function __construct(AccompanyingPeriodWorkRepository $repository)
{
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(
AccompanyingPeriodWorkRepository $repository,
TranslatableStringHelperInterface $translatableStringHelper,
TranslatorInterface $translator
) {
$this->repository = $repository;
$this->translatableStringHelper = $translatableStringHelper;
$this->translator = $translator;
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
return [
'persons' => $this->getRelatedEntity($entityWorkflow)
->getPersons(),
];
}
public function getEntityTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
$work = $this->getRelatedEntity($entityWorkflow);
return
$this->translator->trans('workflow.Work (n°%w%)', ['%w%' => $entityWorkflow->getRelatedEntityId()])
. ' - ' . $this->translatableStringHelper->localize($work->getSocialAction()->getTitle());
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWork
{
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
@@ -48,16 +76,6 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
];
}
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string
{
return '@ChillPerson/Workflow/_accompanying_period_work.title.html.twig';
}
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array
{
return $this->getTemplateData($entityWorkflow, $options);
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWork::class;

View File

@@ -25,7 +25,7 @@ services:
Chill\PersonBundle\Controller\PersonDuplicateController:
arguments:
$similarPersonMatcher: '@Chill\PersonBundle\Search\SimilarPersonMatcher'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
$personRepository: '@Chill\PersonBundle\Repository\PersonRepository'
$personMove: '@Chill\PersonBundle\Actions\Remove\PersonMove'
$eventDispatcher: '@Symfony\Component\EventDispatcher\EventDispatcherInterface'
@@ -66,3 +66,4 @@ services:
Chill\PersonBundle\Controller\RelationshipApiController:
autowire: true
tags: ['controller.service_arguments']

View File

@@ -8,5 +8,5 @@ services:
Chill\PersonBundle\DataFixtures\ORM\LoadCustomFields:
arguments:
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
$translator: '@Symfony\Component\Translation\TranslatorInterface'
$translator: '@Symfony\Contracts\Translation\TranslatorInterface'
tags: [ 'doctrine.fixture.orm' ]

View File

@@ -34,6 +34,10 @@ services:
autowire: true
autoconfigure: true
Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationDocumentVoter:
autowire: true
autoconfigure: true
Chill\PersonBundle\Security\Authorization\HouseholdVoter:
autowire: true
autoconfigure: true

View File

@@ -0,0 +1,49 @@
<?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;
final class Version20220214200327 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP SEQUENCE chill_person_accompanying_period_location_history_id_seq CASCADE');
$this->addSql('DROP TABLE chill_person_accompanying_period_location_history');
}
public function getDescription(): string
{
return 'Add location history to period';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE SEQUENCE chill_person_accompanying_period_location_history_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE chill_person_accompanying_period_location_history (id INT NOT NULL, period_id INT DEFAULT NULL, startDate DATE NOT NULL, endDate DATE DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, personLocation_id INT DEFAULT NULL, addressLocation_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_61E4E688EC8B7ADE ON chill_person_accompanying_period_location_history (period_id)');
$this->addSql('CREATE INDEX IDX_61E4E688D5213D34 ON chill_person_accompanying_period_location_history (personLocation_id)');
$this->addSql('CREATE INDEX IDX_61E4E6889B07D6BF ON chill_person_accompanying_period_location_history (addressLocation_id)');
$this->addSql('CREATE INDEX IDX_61E4E6883174800F ON chill_person_accompanying_period_location_history (createdBy_id)');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_location_history.startDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_location_history.endDate IS \'(DC2Type:date_immutable)\'');
$this->addSql('COMMENT ON COLUMN chill_person_accompanying_period_location_history.createdAt IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER TABLE chill_person_accompanying_period_location_history ADD CONSTRAINT FK_61E4E688EC8B7ADE FOREIGN KEY (period_id) REFERENCES chill_person_accompanying_period (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_location_history ADD CONSTRAINT FK_61E4E688D5213D34 FOREIGN KEY (personLocation_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_location_history ADD CONSTRAINT FK_61E4E6889B07D6BF FOREIGN KEY (addressLocation_id) REFERENCES chill_main_address (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_location_history ADD CONSTRAINT FK_61E4E6883174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('INSERT INTO chill_person_accompanying_period_location_history (id, period_id, startDate, createdAt, personLocation_id, addresslocation_id)
SELECT nextval(\'chill_person_accompanying_period_location_history_id_seq\'), id, NOW(), NOW(), personlocation_id, addresslocation_id FROM chill_person_accompanying_period WHERE step != \'DRAFT\'
');
}
}

View File

@@ -0,0 +1,38 @@
<?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;
/**
* Add title to AccompanyingPeriodWorkEvaluationDocument.
*/
final class Version20220224145951 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_evaluation_document DROP title');
}
public function getDescription(): string
{
return 'Add title to AccompanyingPeriodWorkEvaluationDocument';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_evaluation_document ADD title TEXT NOT NULL DEFAULT \'\'');
$this->addSql('UPDATE chill_person_accompanying_period_work_evaluation_document SET title=name->>\'fr\'
FROM chill_docgen_template cdt WHERE cdt.id = chill_person_accompanying_period_work_evaluation_document.template_id');
}
}

View File

@@ -226,7 +226,7 @@ Concerned scopes: Services concernés
# person resource
person_resources_menu: "Ressources"
person_resources_menu: "Personnes ressources"
Person resources: "Ressources de la personne"
Add a person resource: "Ajouter une ressource"
edit resource: "Modifier ressource"
@@ -240,6 +240,7 @@ Select a type: "Choisissez un type"
Select a person: "Choisissez un usager"
Select a thirdparty: "Choisissez un tiers"
Contact person: "Personne de contact"
Kind: "Type"
# pickAPersonType
@@ -547,6 +548,7 @@ period_notification:
Persons are: Les usagers concernés sont les suivants
Social issues are: Les problématiques sociales renseignées sont les suivantes
See it online: Visualisez le parcours en ligne
Person locating period has moved: L'usager qui localise un parcours a déménagé
You are getting a notification for a period which does not exists any more: Cette notification ne correspond pas à une période d'accompagnement valide.
You are getting a notification for a period you are not allowed to see: La notification fait référence à une période d'accompagnement à laquelle vous n'avez pas accès.
@@ -569,3 +571,6 @@ Linked evaluations: Évaluations associées
# Accompanying period per user
My accompanying periods: Mes parcours
My accompanying periods in draft: Mes parcours brouillons
workflow:
Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%