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

This commit is contained in:
2022-03-24 16:22:52 +01:00
151 changed files with 2587 additions and 636 deletions

View File

@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Controller;
use Chill\ActivityBundle\Entity\Activity;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\AccompanyingCourseType;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
@@ -279,6 +280,40 @@ class AccompanyingCourseController extends Controller
]);
}
/**
* @Route("/{_locale}/person/household/parcours/new", name="chill_household_accompanying_course_new")
*/
public function newHouseholdParcoursAction(Request $request): Response
{
$period = new AccompanyingPeriod();
$em = $this->getDoctrine()->getManager();
if ($request->query->has('household_id')) {
$householdId = $request->query->get('household_id');
$household = $em->getRepository(Household::class)->find($householdId);
$members = $household->getCurrentMembers();
if (null !== $members) {
foreach ($members as $m) {
$period->addPerson($m->getPerson());
}
}
}
$userLocation = $this->getUser()->getCurrentLocation();
$period->setAdministrativeLocation($userLocation);
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::CREATE, $period);
$em->persist($period);
$em->flush();
return $this->redirectToRoute('chill_person_accompanying_course_edit', [
'accompanying_period_id' => $period->getId(),
]);
}
/**
* @Route("/{_locale}/parcours/{accompanying_period_id}/open", name="chill_person_accompanying_course_reopen")
* @ParamConverter("accompanyingCourse", options={"id": "accompanying_period_id"})

View File

@@ -222,6 +222,10 @@ class AccompanyingPeriodController extends AbstractController
$accompanyingPeriodsRaw = $this->accompanyingPeriodACLAwareRepository
->findByPerson($person, AccompanyingPeriodVoter::SEE);
usort($accompanyingPeriodsRaw, static function ($a, $b) {
return $b->getOpeningDate() > $a->getOpeningDate();
});
// filter visible or not visible
$accompanyingPeriods = array_filter($accompanyingPeriodsRaw, function (AccompanyingPeriod $ap) {
return $this->isGranted(AccompanyingPeriodVoter::SEE, $ap);

View File

@@ -134,7 +134,7 @@ class HouseholdCompositionController extends AbstractController
public function index(Household $household, Request $request): Response
{
if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) {
throw new AccessDeniedException('not allowed to edit an household');
throw new AccessDeniedException('not allowed to edit a household');
}
$count = $this->householdCompositionRepository->countByHousehold($household);
@@ -146,6 +146,20 @@ class HouseholdCompositionController extends AbstractController
$paginator->getCurrentPageFirstItemNumber()
);
return new Response($this->engine->render(
'@ChillPerson/HouseholdComposition/index.html.twig',
[
'household' => $household,
'compositions' => $compositions,
]
));
}
/**
* @Route("/{_locale}/person/household/{id}/composition/new", name="chill_person_household_composition_new")
*/
public function newAction(Household $household, Request $request): Response
{
if ($this->security->isGranted(HouseholdVoter::EDIT, $household)) {
$isEdit = $request->query->has('edit');
@@ -195,10 +209,9 @@ class HouseholdCompositionController extends AbstractController
}
return new Response($this->engine->render(
'@ChillPerson/HouseholdComposition/index.html.twig',
'@ChillPerson/HouseholdComposition/create.html.twig',
[
'household' => $household,
'compositions' => $compositions,
'form' => isset($form) ? $form->createView() : null,
'isPosted' => isset($form) ? $form->isSubmitted() : false,
'editId' => $request->query->getInt('edit', -1),

View File

@@ -78,6 +78,10 @@ class HouseholdController extends AbstractController
}
}
usort($accompanyingPeriods, static function ($a, $b) {
return $b->getOpeningDate() > $a->getOpeningDate();
});
$oldMembers = $household->getNonCurrentMembers();
$accompanyingPeriodsOld = [];

View File

@@ -133,6 +133,19 @@ final class PersonResourceController extends AbstractController
$personResources = [];
$personResources = $this->personResourceRepository->findBy(['personOwner' => $personOwner->getId()]);
return $this->render(
'ChillPersonBundle:PersonResource:list.html.twig',
[
'person' => $personOwner,
'personResources' => $personResources,
]
);
}
public function newAction(Request $request, $person_id)
{
$personOwner = $this->personRepository->find($person_id);
$form = $this->createForm(PersonResourceType::class);
$form->handleRequest($request);
@@ -165,11 +178,10 @@ final class PersonResourceController extends AbstractController
}
return $this->render(
'ChillPersonBundle:PersonResource:list.html.twig',
'ChillPersonBundle:PersonResource:create.html.twig',
[
'person' => $personOwner,
'personResources' => $personResources,
'form' => $form->createView(),
'person' => $personOwner,
]
);
}

View File

@@ -0,0 +1,114 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\UserRepository;
use Chill\MainBundle\Templating\Entity\UserRender;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepositoryInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\FormType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
class ReassignAccompanyingPeriodController extends AbstractController
{
private AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository;
private EngineInterface $engine;
private FormFactoryInterface $formFactory;
private PaginatorFactory $paginatorFactory;
private Security $security;
private UserRender $userRender;
private UserRepository $userRepository;
public function __construct(AccompanyingPeriodACLAwareRepositoryInterface $accompanyingPeriodACLAwareRepository, UserRepository $userRepository, EngineInterface $engine, FormFactoryInterface $formFactory, PaginatorFactory $paginatorFactory, Security $security, UserRender $userRender)
{
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->engine = $engine;
$this->formFactory = $formFactory;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security;
$this->userRepository = $userRepository;
$this->userRender = $userRender;
}
/**
* @Route("/{_locale}/person/accompanying-periods/reassign", name="chill_course_list_reassign")
*/
public function listAction(Request $request): Response
{
if (!$this->security->isGranted('ROLE_USER') || !$this->security->getUser() instanceof User) {
throw new AccessDeniedException();
}
$form = $this->buildFilterForm();
$form->handleRequest($request);
$total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod(
$form['user']->getData()
);
$paginator = $this->paginatorFactory->create($total);
$periods = $this->accompanyingPeriodACLAwareRepository
->findByUserOpenedAccompanyingPeriod(
$form['user']->getData(),
['openingDate' => 'ASC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new Response(
$this->engine->render('@ChillPerson/AccompanyingPeriod/reassign_list.html.twig', [
'paginator' => $paginator,
'periods' => $periods,
'form' => $form->createView(),
])
);
}
private function buildFilterForm(): FormInterface
{
$data = [
'user' => null,
];
$builder = $this->formFactory->createBuilder(FormType::class, $data, [
'method' => 'get', 'csrf_protection' => false, ]);
$builder
->add('user', EntityType::class, [
'class' => User::class,
'choices' => $this->userRepository->findByActive(['username' => 'ASC']),
'choice_label' => function (User $u) {
return $this->userRender->renderString($u, []);
},
'multiple' => false,
'label' => 'User',
'required' => false,
]);
return $builder->getForm();
}
}

View File

@@ -13,7 +13,7 @@ namespace Chill\PersonBundle\Controller;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\Person\ResidentialAddress;
use Chill\PersonBundle\Form\Type\ResidentialAddressType;
use Chill\PersonBundle\Form\ResidentialAddressType;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;

View File

@@ -15,6 +15,7 @@ use Chill\MainBundle\DependencyInjection\MissingBundleException;
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
use Chill\PersonBundle\Controller\HouseholdCompositionTypeApiController;
use Chill\PersonBundle\Doctrine\DQL\AddressPart;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodCommentVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
@@ -58,6 +59,16 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
$config['allow_multiple_simultaneous_accompanying_periods']
);
$container->setParameter(
'chill_person.create_person_allowed',
$config['create_person_allowed']
);
$container->setParameter(
'chill_person.create_parcours_allowed',
$config['create_parcours_allowed']
);
// register all configuration in a unique parameter
$container->setParameter('chill_person', $config);
@@ -415,6 +426,25 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
],
],
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Comment::class,
'name' => 'accompanying_period_comment',
'base_path' => '/api/1.0/person/accompanying-period/comment',
'base_role' => 'ROLE_USER',
'actions' => [
'_entity' => [
'methods' => [
Request::METHOD_GET => false,
Request::METHOD_PATCH => true,
Request::METHOD_HEAD => false,
Request::METHOD_DELETE => false,
],
'roles' => [
Request::METHOD_PATCH => AccompanyingPeriodCommentVoter::EDIT,
],
],
],
],
[
'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\Resource::class,
'name' => 'accompanying_period_resource',

View File

@@ -62,6 +62,12 @@ class Configuration implements ConfigurationInterface
->end() // children for 'validation', parent = validation
->end() //validation, parent = children of root
->end() // children of root, parent = root
->booleanNode('create_person_allowed')
->defaultTrue()
->end()
->booleanNode('create_parcours_allowed')
->defaultTrue()
->end()
->arrayNode('person_fields')
->canBeDisabled()
->children()

View File

@@ -257,9 +257,11 @@ class AccompanyingPeriod implements
/**
* @ORM\ManyToOne(
* targetEntity=Comment::class
* targetEntity=Comment::class,
* cascade={"persist"},
* )
* @Groups({"read"})
* @ORM\JoinColumn(onDelete="SET NULL")
*/
private ?Comment $pinnedComment = null;

View File

@@ -142,6 +142,15 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
*/
private Collection $persons;
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_referrer")
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
private Collection $referrers;
/**
* @ORM\ManyToMany(targetEntity=Result::class, inversedBy="accompanyingPeriodWorks")
* @ORM\JoinTable(name="chill_person_accompanying_period_work_result")
@@ -196,6 +205,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
$this->thirdParties = new ArrayCollection();
$this->persons = new ArrayCollection();
$this->accompanyingPeriodWorkEvaluations = new ArrayCollection();
$this->referrers = new ArrayCollection();
}
public function addAccompanyingPeriodWorkEvaluation(AccompanyingPeriodWorkEvaluation $evaluation): self
@@ -227,6 +237,15 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function addReferrer(User $referrer): self
{
if (!$this->referrers->contains($referrer)) {
$this->referrers[] = $referrer;
}
return $this;
}
public function addResult(Result $result): self
{
if (!$this->results->contains($result)) {
@@ -308,6 +327,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this->persons;
}
/**
* @return Collection|User[]
*/
public function getReferrers(): Collection
{
return $this->referrers;
}
/**
* @return Collection|Result[]
*/
@@ -382,6 +409,13 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
return $this;
}
public function removeReferrer(User $referrer): self
{
$this->referrers->removeElement($referrer);
return $this;
}
public function removeResult(Result $result): self
{
$this->results->removeElement($result);

View File

@@ -124,6 +124,7 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"write"})
* @Serializer\Groups({"accompanying_period_work_evaluation:create"})
*/
private ?DateTimeImmutable $maxDate = null;

View File

@@ -28,6 +28,7 @@ use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress;
use Chill\PersonBundle\Entity\Person\PersonCurrentAddress;
use Chill\PersonBundle\Entity\Person\PersonResource;
use Chill\PersonBundle\Validator\Constraints\Household\HouseholdMembershipSequential;
use Chill\PersonBundle\Validator\Constraints\Person\Birthdate;
use Chill\PersonBundle\Validator\Constraints\Person\PersonHasCenter;
@@ -111,6 +112,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @ORM\OneToMany(targetEntity=AccompanyingPeriodParticipation::class,
* mappedBy="person",
* cascade={"persist", "remove", "merge", "detach"})
* @ORM\OrderBy({"startDate": "DESC"})
*/
private $accompanyingPeriodParticipations;
@@ -468,6 +470,13 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
*/
private $proxyAccompanyingPeriodOpenState = false; //TO-DELETE ?
/**
* @ORM\OneToMany(targetEntity=PersonResource::class, mappedBy="personOwner")
*
* @var Collection|PersonResource[];
*/
private Collection $resources;
/**
* The person's spoken languages.
*
@@ -512,6 +521,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
$this->accompanyingPeriodRequested = new ArrayCollection();
$this->budgetResources = new ArrayCollection();
$this->budgetCharges = new ArrayCollection();
$this->resources = new ArrayCollection();
}
/**
@@ -1272,7 +1282,20 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->placeOfBirth;
}
public function getSpokenLanguages(): ?Collection
/**
* @return PersonResource[]|Collection
*/
public function getResources()
{
return $this->resources;
}
/**
* Get spokenLanguages.
*
* @return ArrayCollection
*/
public function getSpokenLanguages()
{
return $this->spokenLanguages;
}

View File

@@ -19,7 +19,7 @@ use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Entity\Person;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\Groups;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
@@ -27,7 +27,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
/**
* @ORM\Entity
* @ORM\Table(name="chill_person_resource")
* @DiscriminatorMap(typeProperty="type", mapping={
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "personResource": personResource::class
* })
*/
@@ -39,13 +39,13 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
* @Groups({"read"})
* @Serializer\Groups({"read", "docgen:read"})
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="text", nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?string $freeText = null;
@@ -53,25 +53,30 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Groups({"read", "docgen:read"})
*/
private ?int $id;
/**
* @ORM\ManyToOne(targetEntity=PersonResourceKind::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private $kind;
private ?PersonResourceKind $kind = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="personResources")
* The person which host the owner of this resource.
*
* @ORM\ManyToOne(targetEntity=Person::class)
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?Person $person = null;
/**
* @ORM\ManyToOne(targetEntity=Person::class)
* The person linked with this resource.
*
* @ORM\ManyToOne(targetEntity=Person::class, inversedBy="resources")
* @ORM\JoinColumn(nullable=false)
* @Groups({"read"})
*/
@@ -80,7 +85,7 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
/**
* @ORM\ManyToOne(targetEntity=ThirdParty::class, inversedBy="personResources")
* @ORM\JoinColumn(nullable=true)
* @Groups({"read"})
* @Groups({"read", "docgen:read"})
*/
private ?ThirdParty $thirdParty = null;
@@ -122,6 +127,26 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
return $this->personOwner;
}
/**
* @Groups({"read", "docgen:read"})
*/
public function getResourceKind(): string
{
if ($this->getPerson() instanceof Person) {
return 'person';
}
if ($this->getThirdParty() instanceof ThirdParty) {
return 'thirdparty';
}
if (null !== $this->getFreeText()) {
return 'freetext';
}
return 'none';
}
public function getThirdParty(): ?ThirdParty
{
return $this->thirdParty;
@@ -205,5 +230,10 @@ class PersonResource implements TrackCreationInterface, TrackUpdateInterface
$context->buildViolation('You must associate at least one entity')
->addViolation();
}
if (null !== $this->person && $this->person === $this->personOwner) {
$context->buildViolation('You cannot associate a resource with the same person')
->addViolation();
}
}
}

View File

@@ -12,10 +12,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Person;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* **About denormalization**: this operation is operated by @see{AccompanyingPeriodResourdeNormalizer}.
*
* @ORM\Entity
* @ORM\Table(name="chill_person_resource_kind")
*/
@@ -25,8 +24,9 @@ class PersonResourceKind
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"docgen:read"})
*/
private int $id;
private ?int $id = null;
/**
* @ORM\Column(type="boolean")
@@ -35,6 +35,8 @@ class PersonResourceKind
/**
* @ORM\Column(type="json", length=255)
* @Serializer\Groups({"docgen:read"})
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
*/
private array $title;

View File

@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Entity;
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
/**
* Person Phones.
@@ -51,9 +52,9 @@ class PersonPhone
private Person $person;
/**
* @ORM\Column(type="text", length=40, nullable=false)
* @ORM\Column(type="phone_number", nullable=false)
*/
private string $phonenumber = '';
private ?PhoneNumber $phonenumber = null;
/**
* @ORM\Column(type="text", length=40, nullable=true)
@@ -85,7 +86,7 @@ class PersonPhone
return $this->person;
}
public function getPhonenumber(): string
public function getPhonenumber(): ?PhoneNumber
{
return $this->phonenumber;
}
@@ -97,7 +98,8 @@ class PersonPhone
public function isEmpty(): bool
{
return empty($this->getDescription()) && empty($this->getPhonenumber());
return ('' === $this->getDescription() || null === $this->getDescription())
&& null === $this->getPhonenumber();
}
public function setDate(DateTime $date): void
@@ -115,7 +117,7 @@ class PersonPhone
$this->person = $person;
}
public function setPhonenumber(string $phonenumber): void
public function setPhonenumber(?PhoneNumber $phonenumber): void
{
$this->phonenumber = $phonenumber;
}

View File

@@ -62,6 +62,12 @@ class Evaluation
*/
private array $title = [];
/**
* @ORM\Column(type="text", nullable=true)
* @Serializer\Groups({"read", "docgen:read"})
*/
private ?string $url = null;
public function __construct()
{
$this->socialActions = new ArrayCollection();
@@ -101,6 +107,11 @@ class Evaluation
return $this->title;
}
public function getUrl(): ?string
{
return $this->url;
}
public function removeSocialAction(SocialAction $socialAction): self
{
if ($this->socialActions->contains($socialAction)) {
@@ -130,4 +141,11 @@ class Evaluation
return $this;
}
public function setUrl(?string $url): self
{
$this->url = $url;
return $this;
}
}

View File

@@ -0,0 +1,33 @@
<?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\EventListener;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Symfony\Component\Security\Core\Security;
class AccompanyingPeriodWorkEventListener
{
private Security $security;
public function __construct(Security $security)
{
$this->security = $security;
}
public function prePersistAccompanyingPeriodWork(AccompanyingPeriodWork $work): void
{
if ($this->security->getUser() instanceof User) {
$work->addReferrer($this->security->getUser());
}
}
}

View File

@@ -27,6 +27,7 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone;
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 Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
@@ -158,7 +159,7 @@ class PersonType extends AbstractType
}
$builder->add('otherPhoneNumbers', ChillCollectionType::class, [
'entry_type' => ChillPhoneNumberType::class,
'entry_type' => PersonPhoneType::class,
'button_add_label' => 'Add new phone',
'button_remove_label' => 'Remove phone',
'required' => false,

View File

@@ -9,11 +9,12 @@
declare(strict_types=1);
namespace Chill\PersonBundle\Form\Type;
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\PersonBundle\Entity\Person\ResidentialAddress;
use Chill\PersonBundle\Form\Type\PickPersonDynamicType;
use Chill\ThirdPartyBundle\Form\Type\PickThirdpartyDynamicType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\DateType;

View File

@@ -11,11 +11,11 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Form\Type;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
use Chill\PersonBundle\Entity\PersonPhone;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
@@ -36,7 +36,7 @@ class PersonPhoneType extends AbstractType
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('phonenumber', TelType::class, [
$builder->add('phonenumber', ChillPhoneNumberType::class, [
'label' => 'Other phonenumber',
'required' => true,
]);

View File

@@ -68,12 +68,14 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
}
if ($this->security->isGranted(AccompanyingPeriodVoter::SEE_DETAILS, $period)) {
$menu->addChild($this->translator->trans('Accompanying Course History'), [
'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
], ])
->setExtras(['order' => 30]);
/*
$menu->addChild($this->translator->trans('Accompanying Course History'), [
'route' => 'chill_person_accompanying_course_history',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
], ])
->setExtras(['order' => 30]);
*/
$menu->addChild($this->translator->trans('Accompanying Course Comment'), [
'route' => 'chill_person_accompanying_period_comment_list',

View File

@@ -13,7 +13,9 @@ namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Doctrine\ORM\Query\Parameter;
use Knp\Menu\MenuItem;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -22,23 +24,20 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class SectionMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
protected $authorizationChecker;
protected AuthorizationCheckerInterface $authorizationChecker;
/**
* @var TranslatorInterface
*/
protected $translator;
protected TranslatorInterface $translator;
protected ParameterBagInterface $parameterBag;
/**
* SectionMenuBuilder constructor.
*/
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator)
public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator, ParameterBagInterface $parameterBag)
{
$this->authorizationChecker = $authorizationChecker;
$this->translator = $translator;
$this->parameterBag = $parameterBag;
}
/**
@@ -46,7 +45,7 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
*/
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if ($this->authorizationChecker->isGranted(PersonVoter::CREATE)) {
if ($this->authorizationChecker->isGranted(PersonVoter::CREATE) && $this->parameterBag->get('chill_person.create_person_allowed')) {
$menu->addChild($this->translator->trans('Add a person'), [
'route' => 'chill_person_new',
])
@@ -56,12 +55,22 @@ class SectionMenuBuilder implements LocalMenuBuilderInterface
]);
}
$menu->addChild($this->translator->trans('Create an accompanying course'), [
'route' => 'chill_person_accompanying_course_new',
if ($this->parameterBag->get('chill_person.create_parcours_allowed')) {
$menu->addChild($this->translator->trans('Create an accompanying course'), [
'route' => 'chill_person_accompanying_course_new',
])
->setExtras([
'order' => 11,
'icons' => ['plus'],
]);
}
$menu->addChild($this->translator->trans('Accompanying courses of users'), [
'route' => 'chill_course_list_reassign',
])
->setExtras([
'order' => 11,
'icons' => ['plus'],
'order' => 12,
'icons' => ['task'],
]);
}

View File

@@ -87,9 +87,12 @@ class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
->join('work.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isNull('e.endDate'),
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval'))
$qb->expr()->gte(':now', $qb->expr()->diff('e.maxDate', 'e.warningInterval')),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'work.referrers')
)
)
)
->setParameters([

View File

@@ -159,9 +159,12 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
->join('w.accompanyingPeriod', 'period')
->where(
$qb->expr()->andX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->gte('w.endDate', ':since'),
$qb->expr()->lte('w.startDate', ':until')
$qb->expr()->lte('w.startDate', ':until'),
$qb->expr()->orX(
$qb->expr()->eq('period.user', ':user'),
$qb->expr()->isMemberOf(':user', 'w.referrers')
)
)
)
->setParameters([

View File

@@ -11,12 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
use Symfony\Component\Security\Core\Security;
use DateTime;
use Symfony\Component\Security\Core\Security;
use function count;
final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodACLAwareRepositoryInterface
@@ -41,6 +43,37 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
$this->centerResolverDispatcher = $centerResolverDispatcher;
}
public function buildQueryOpenedAccompanyingCourseByUser(?User $user)
{
$qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap');
$qb->where($qb->expr()->eq('ap.user', ':user'))
->andWhere(
$qb->expr()->neq('ap.step', ':draft'),
$qb->expr()->orX(
$qb->expr()->isNull('ap.closingDate'),
$qb->expr()->gt('ap.closingDate', ':now')
)
)
->setParameter('user', $user)
->setParameter('now', new DateTime('now'))
->setParameter('draft', AccompanyingPeriod::STEP_DRAFT);
return $qb;
}
public function countByUserOpenedAccompanyingPeriod(?User $user): int
{
if (null === $user) {
return 0;
}
return $this->buildQueryOpenedAccompanyingCourseByUser($user)
->select('COUNT(ap)')
->getQuery()
->getSingleScalarResult();
}
public function findByPerson(
Person $person,
string $role,
@@ -92,4 +125,25 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC
return $qb->getQuery()->getResult();
}
/**
* @return array|AccompanyingPeriod[]
*/
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array
{
if (null === $user) {
return [];
}
$qb = $this->buildQueryOpenedAccompanyingCourseByUser($user);
$qb->setFirstResult($offset)
->setMaxResults($limit);
foreach ($orderBy as $field => $direction) {
$qb->addOrderBy('ap.' . $field, $direction);
}
return $qb->getQuery()->getResult();
}
}

View File

@@ -11,10 +11,13 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\Person;
interface AccompanyingPeriodACLAwareRepositoryInterface
{
public function countByUserOpenedAccompanyingPeriod(?User $user): int;
public function findByPerson(
Person $person,
string $role,
@@ -22,4 +25,6 @@ interface AccompanyingPeriodACLAwareRepositoryInterface
?int $limit = null,
?int $offset = null
): array;
public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array;
}

View File

@@ -78,6 +78,16 @@ final class AccompanyingPeriodRepository implements ObjectRepository
->getResult();
}
public function findConfirmedByUser(User $user)
{
$qb = $this->createQueryBuilder('ap');
$qb->where($qb->expr()->eq('ap.user', ':user'))
->andWhere('ap.step', 'CONFIRMED')
->setParameter('user', $user);
return $qb;
}
public function findOneBy(array $criteria): ?AccompanyingPeriod
{
return $this->findOneBy($criteria);

View File

@@ -33,61 +33,74 @@ div.banner {
padding-top: 1em;
padding-bottom: 1em;
div.contact {
display: flex;
align-content: center;
& > * {
margin-right: 1em;
}
}
.household-link {
border: 1px solid white;
padding: .05rem .3rem;
border-radius: 5px;
color: white;
cursor: pointer;
&:hover {
background-color: white;
color: $chill-person-context
}
}
}
}
div.person-view {
figure.person-details {
h2 {
font-family: 'Open Sans';
font-weight: 600;
margin-bottom: 0.3em;
font-variant: small-caps;
}
dl {
margin-top: 0.3em;
}
dt {
font-family: 'Open Sans';
font-weight: 600;
}
dd {
margin-left: 0;
}
/*
a.sc-button { background-color: $black; padding-top: 0.2em; padding-bottom: 0.2em; }
*/
}
/* custom fields on the home page */
div.custom-fields {
figure.person-details {
display: flex;
flex-flow: row wrap;
div.cf_title_box:nth-child(4n+1) h2 {
@extend .chill-red !optional;
}
div.cf_title_box:nth-child(4n+2) h2 {
@extend .chill-green !optional;
}
div.cf_title_box:nth-child(4n+3) h2 {
@extend .chill-orange !optional;
}
div.cf_title_box:nth-child(4n+4) h2 {
@extend .chill-blue !optional;
}
div.cf_title_box:nth-child(2n+1) {
width: 50%;
margin-right: 40px;
}
div.cf_title_box:nth-child(2n+2) {
width: calc(50% - 40px);
}
}
}
figure.person-details {
h2 {
font-family: 'Open Sans';
font-weight: 600;
margin-bottom: 0.3em;
font-variant: small-caps;
}
dl {
margin-top: 0.3em;
}
dt {
font-family: 'Open Sans';
font-weight: 600;
}
dd {
margin-left: 0;
}
/*
a.sc-button { background-color: $black; padding-top: 0.2em; padding-bottom: 0.2em; }
*/
}
/* custom fields on the home page */
div.custom-fields {
figure.person-details {
display: flex;
flex-flow: row wrap;
div.cf_title_box:nth-child(4n+1) h2 {
@extend .chill-red !optional;
}
div.cf_title_box:nth-child(4n+2) h2 {
@extend .chill-green !optional;
}
div.cf_title_box:nth-child(4n+3) h2 {
@extend .chill-orange !optional;
}
div.cf_title_box:nth-child(4n+4) h2 {
@extend .chill-blue !optional;
}
div.cf_title_box:nth-child(2n+1) {
width: 50%;
margin-right: 40px;
}
div.cf_title_box:nth-child(2n+2) {
width: calc(50% - 40px);
}
}
}
}
/*

View File

@@ -19,22 +19,21 @@ import {fetchResults} from 'ChillMainAssets/lib/api/apiMethods.js';
*/
document.querySelectorAll('[data-set-referrer-app]').forEach(function (el) {
let
periodId = Number.parseInt(el.dataset.setReferrerAccompanyingPeriodId);
const periodId = Number.parseInt(el.dataset.setReferrerAccompanyingPeriodId);
const jobId = Number.parseInt(el.dataset.setReferrerJobId);
const url = `/api/1.0/person/accompanying-course/${periodId}/referrers-suggested.json`;
fetchResults(url).then(suggested => {
const filteredSuggested = suggested.filter((s) => s.user_job ? s.user_job.id === jobId : false);
const app = createApp({
components: {
SetReferrer,
},
template:
'<set-referrer :suggested="suggested" :periodId="periodId" @referrerSet="onReferrerSet"></set-referrer>',
'<set-referrer :suggested="filteredSuggested" :periodId="periodId" @referrerSet="onReferrerSet"></set-referrer>',
data() {
return {
periodId, suggested, original: suggested,
periodId, filteredSuggested, original: filteredSuggested,
}
},
methods: {
@@ -56,7 +55,7 @@ document.querySelectorAll('[data-set-referrer-app]').forEach(function (el) {
label.textContent = ref.text;
label.classList.remove('chill-no-data-statement');
this.suggested = this.original.filter(user => user.id !== ref.id);
this.filteredSuggested = this.original.filter(user => user.id !== ref.id);
}
}
});

View File

@@ -14,24 +14,27 @@
<ckeditor
name="content"
v-bind:placeholder="$t('comment.content')"
:placeholder="$t('comment.content')"
:editor="editor"
v-model="content"
tag-name="textarea">
</ckeditor>
<div v-if="pinnedComment" class="metadata">
{{ $t('comment.created_by', [
pinnedComment.creator.text,
$d(pinnedComment.createdAt.datetime, 'long')
]) }}
<div class="sub-comment">
<div v-if="pinnedComment !== null && typeof pinnedComment.creator !== 'undefined'" class="metadata">
{{ $t('comment.created_by', [
pinnedComment.creator.text,
$d(pinnedComment.updatedAt.datetime, 'long')
])
}}
</div>
<div class="loading">
<i v-if="loading" class="fa fa-circle-o-notch fa-spin" :title="$t('loading')"></i>
</div>
</div>
<div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save">{{ $t('action.save') }}</button>
</li>
<li v-if="pinnedComment !== null">
<a class="btn btn-delete"
@click="removeComment">
@@ -50,6 +53,7 @@
<script>
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
import { mapState } from "vuex";
export default {
name: "Comment",
@@ -59,22 +63,58 @@ export default {
data() {
return {
editor: ClassicEditor,
formdata: {
type: "accompanying_period_comment",
content: ''
}
loading: false,
lastRecordedContent: null,
}
},
computed: {
pinnedComment() {
return this.$store.state.accompanyingCourse.pinnedComment;
},
...mapState({
pinnedComment: state => state.accompanyingCourse.pinnedComment,
}),
content: {
set(value) {
this.formdata.content = value;
console.log('new comment value', value);
console.log('previous value', this.lastRecordedContent);
this.lastRecordedContent = value;
setTimeout(() => {
console.log('performing test on ', value);
if (this.lastRecordedContent === value) {
this.loading = true;
if (value !== '') {
this.$store.dispatch('updatePinnedComment', value)
.then(() => {
this.loading = false;
})
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
} else {
if (this.$store.state.accompanyingCourse.pinnedComment !== null) {
this.$store.dispatch('removePinnedComment', {id: this.pinnedComment.id})
.then(() => {
this.loading = false;
this.lastRecoredContent = null;
})
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
}
}
}
}, 3000);
},
get() {
return (this.pinnedComment)? this.pinnedComment.content : {};
get() {
return this.pinnedComment ? this.pinnedComment.content : '';
}
},
errors() {
@@ -82,18 +122,11 @@ export default {
}
},
methods: {
submitform() {
this.$store.dispatch('postFirstComment', this.formdata)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
onContentChange() {
let lastRecordedContent = this.formData.content;
},
removeComment() {
this.$store.dispatch('postFirstComment', {})
this.$store.dispatch('removePinnedComment', {id: this.pinnedComment.id})
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
@@ -104,15 +137,18 @@ export default {
}
}
}
/*
* TODO
* - [x] delete button in ul record_actions, but not in form
* - [ ] display updatedAt => pinnedComment fetch PATCH content changes MUST NOT change object id !!
*/
</script>
<style lang="scss">
div.ck-editor.ck-reset {
margin: 0.6em 0;
}
div.sub-comment {
display: flex;
justify-content: space-between;
div.loading {
margin-right: 6px;
margin-left: 6px;
}
}
</style>

View File

@@ -59,14 +59,6 @@
ref="addAddress">
</add-address>
</li>
<li v-if="isPersonLocation">
<button
class="btn btn-remove"
@click="removeAddress"
:title="$t('courselocation.remove_button')">
{{ $t('action.remove') }}
</button>
</li>
</ul>
</div>
@@ -180,22 +172,6 @@ export default {
}
this.$store.commit('setAddressContext', context);
},
removeAddress() {
let payload = {
target: this.context.target.name,
targetId: this.context.target.id,
locationStatusTo: 'none'
};
//console.log('remove address');
this.$store.dispatch('updateLocation', payload)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
displayErrors() {
return this.$refs.addAddress.errorMsg;
},

View File

@@ -9,7 +9,7 @@
<input type="checkbox" v-model="requestorIsAnonymous" class="me-2" />
{{ $t('requestor.is_anonymous') }}
</label>
<confidential :positionBtn="false" v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<confidential v-if="accompanyingCourse.requestor.type === 'thirdparty'">
<template v-slot:confidential-content>
<third-party-render-box
:thirdparty="accompanyingCourse.requestor"
@@ -33,7 +33,7 @@
</template>
</confidential>
<confidential :positionBtnFar="false" v-else-if="accompanyingCourse.requestor.type === 'person'">
<confidential v-else-if="accompanyingCourse.requestor.type === 'person'">
<template v-slot:confidential-content>
<person-render-box render="bloc"
:person="accompanyingCourse.requestor"
@@ -339,5 +339,6 @@ div.flex-table {
.confidential {
display: block;
margin-right: 0px !important;
}
</style>

View File

@@ -170,7 +170,9 @@ export default {
console.log('data', payload.data)
body.name = payload.data.name;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.telephone = payload.data.telephone;
body.civility = payload.data.civility;
body.profession = payload.data.profession;
body.address = payload.data.address ? { id: payload.data.address.address_id } : null;
makeFetch('PATCH', `/api/1.0/thirdparty/thirdparty/${payload.data.id}.json`, body)

View File

@@ -122,7 +122,7 @@ const appMessages = {
title: "Observations",
label: "Ajout d'une note",
content: "Rédigez une première note…",
created_by: "créé par {0}, le {1}"
created_by: "créé par {0}, mis à jour le {1}"
},
confirm: {
title: "Confirmation",

View File

@@ -42,7 +42,11 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
referrersSuggested: [],
// all the users available
users: [],
permissions: {}
permissions: {},
// controller for updating comment
updateCommentAbortController: null,
// waiting response for inserting first comment
postFirstPinnedCommentResponse: null,
},
getters: {
isParticipationValid(state) {
@@ -203,9 +207,35 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
//console.log('### mutation: toggleConfidential');
state.accompanyingCourse.confidential = value;
},
postFirstComment(state, comment) {
//console.log('### mutation: postFirstComment', comment);
state.accompanyingCourse.pinnedComment = comment;
setPinnedComment(state, content) {
if (null === state.accompanyingCourse.pinnedComment) {
state.accompanyingCourse.pinnedComment = {
id: -1,
content,
type: "accompanying_period_comment",
};
} else {
state.accompanyingCourse.pinnedComment.content = content;
}
},
setPinnedCommentDetails(state, value) {
state.accompanyingCourse.pinnedComment.id = value.id;
if (typeof value.creator !== 'undefined') {
state.accompanyingCourse.pinnedComment.creator = value.creator;
state.accompanyingCourse.pinnedComment.updatedBy = value.updatedBy;
state.accompanyingCourse.pinnedComment.updatedAt = value.updatedAt;
state.accompanyingCourse.pinnedComment.createdAt = value.createdAt;
}
},
removePinnedComment(state, value) {
state.accompanyingCourse.pinnedComment = null;
},
setPinCommentAbortController(state, value) {
state.updateCommentAbortController = value;
},
setPostFirstPinnedCommentResponse(state, value) {
state.postFirstPinnedCommentResponse = value;
},
updateSocialIssues(state, value) {
console.log('updateSocialIssues', value);
@@ -337,6 +367,87 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
throw error;
})
},
/**
* Add/remove pinnedComment
*/
removePinnedComment({ commit }, payload) {
const body = {type: "accompanying_period_comment", id: payload.id};
const url = `/api/1.0/person/accompanying-course/${id}/comment.json`;
return makeFetch('DELETE', url, body)
.then((response) => {
commit('removePinnedComment');
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
/**
* internal method to insert a new comment
*
* @param commit
* @param dispatch
* @param content
* @returns {*}
*/
addPinnedComment({ commit, dispatch }, content) {
const url = `/api/1.0/person/accompanying-course/${id}/comment.json`;
return makeFetch('POST', url, {type: "accompanying_period_comment", content})
.then((response) => {
commit('setPinnedCommentDetails', response);
return dispatch('patchFirstComment', response);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
updatePinnedComment({ commit, state, dispatch }, content) {
commit('setPinnedComment', content);
if (state.accompanyingCourse.pinnedComment.id === -1 && state.postFirstPinnedCommentResponse === null) {
let r = dispatch('addPinnedComment', content).then(() => {
commit('setPostFirstPinnedCommentResponse', null);
});
commit('setPostFirstPinnedCommentResponse', r);
} else {
(state.postFirstPinnedCommentResponse === null ? Promise.resolve() : state.postFirstPinnedCommentResponse).then(() => {
dispatch('updateExistingPinnedComment', content);
});
}
},
/**
* internal method to patch an existing comment
*
* @param commit
* @param state
* @param comment
* @returns {*}
*/
updateExistingPinnedComment({ commit, state }, content) {
const payload = {type: "accompanying_period_comment", content, id: state.accompanyingCourse.pinnedComment.id};
const url = `/api/1.0/person/accompanying-period/comment/${payload.id}.json`;
if (state.updateCommentAbortController !== null) {
state.updateCommentAbortController.abort();
commit('setPinCommentAbortController', null);
}
let controller = new AbortController();
commit('setPinCommentAbortController', controller);
return makeFetch('PATCH', url, payload, { signal: controller.signal })
.then((response) => {
commit('setPinCommentAbortController', null);
commit('setPinnedCommentDetails', response);
})
.catch((error) => {
commit('catchError', error);
commit('setPinCommentAbortController', null);
throw error;
})
},
/**
* Add/remove/display anonymous requestor
*/
@@ -606,18 +717,20 @@ let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCou
return Promise.all(promises);
},
postFirstComment({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const body = { type: "accompanying_period", pinnedComment: payload }
patchFirstComment({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = {
type: "accompanying_period",
pinnedComment: {
type: "accompanying_period_comment",
id: payload.id
}
};
return makeFetch('PATCH', url, body)
.then((response) => {
commit('postFirstComment', response.pinnedComment);
})
.catch((error) => {
commit('catchError', error);
throw error;
})
});
},
updateSocialIssues({ state, commit, dispatch }, { payload, body, method }) {
const url = `/api/1.0/person/accompanying-course/${id}/socialissue.json`;

View File

@@ -127,7 +127,7 @@
</div>
<ul class="record_actions" v-if="evaluationsForAction.length > 0">
<li>
<button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation"></button>
<button :title="$t('add_an_evaluation')" class="btn btn-create" @click="toggleAddEvaluation">{{ $t('add_an_evaluation') }}</button>
</li>
</ul>
<div v-else>
@@ -151,6 +151,37 @@
</ul>
</div>
<div id="referrers" class="action-row">
<h3>{{ $t('referrers') }}</h3>
<div v-if="!hasReferrers">
<p class="chill-no-data-statement">{{ $t('no_referrers') }}</p>
</div>
<div v-else>
<ul class="list-suggest remove-items inline">
<li v-for="u in referrers" :key="u.id" :title="$t('remove_referrer')" @click="removeReferrer(u)">
<span>
{{ u.text }}
</span>
</li>
</ul>
</div>
<ul class="record_actions">
<li class="add-persons">
<add-persons
ref="referrerPicker"
:key="referrerPicker.key"
:buttonTitle="$t('add_referrers')"
:modalTitle="$t('choose_referrers')"
:options="referrerPicker.options"
@addNewPersons="addReferrers">
</add-persons>
</li>
</ul>
</div>
<div id="handlingThirdParty" class="action-row">
<h3>{{ $t('handling_thirdparty') }}</h3>
@@ -289,7 +320,6 @@ import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vu
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
const i18n = {
messages: {
fr: {
@@ -319,9 +349,13 @@ const i18n = {
add_thirdparties: "Ajouter des tiers",
choose_thirdparties: "Choisir des tiers",
fix_these_errors: "Veuillez corriger les erreurs suivantes :",
available_evaluations_text: "Évaluations disponibles pour ajout :",
available_evaluations_text: "Documents disponibles pour ajout :",
no_evaluations_available: "Aucune évaluation disponible",
no_goals_available: "Aucun objectif disponible",
referrers: "Agents traitants",
no_referrers: "Aucun agent traitant",
choose_referrers: "Choisir des agents traitants",
remove_referrer: "Enlever l'agent"
}
}
};
@@ -370,6 +404,17 @@ export default {
}
},
},
referrerPicker: {
key: 'referrer',
options: {
type: ['user'],
priority: null,
uniq: false,
button: {
display: false
}
},
},
};
},
computed: {
@@ -381,6 +426,7 @@ export default {
'personsReachables',
'handlingThirdParty',
'thirdParties',
'referrers',
'isPosting',
'errors',
'templatesAvailablesForAction',
@@ -389,6 +435,7 @@ export default {
'hasResultsForAction',
'hasHandlingThirdParty',
'hasThirdParties',
'hasReferrers'
]),
startDate: {
get() {
@@ -465,6 +512,14 @@ export default {
removeThirdParty(t) {
this.$store.commit('removeThirdParty', t);
},
addReferrers({selected, modal}) {
this.$store.commit('addReferrers', selected.map(r => r.result));
this.$refs.referrerPicker.resetSearch();
modal.showModal = false;
},
removeReferrer(u) {
this.$store.commit('removeReferrer', u);
},
goToGenerateWorkflow({link}) {
console.log('save before leave to generate workflow')
const callback = (data) => {
@@ -521,6 +576,7 @@ div#workEditor {
"objectives objectives"
"evaluations evaluations"
"persons persons"
"referrers referrers"
"handling handling"
"tparties tparties"
"errors errors";
@@ -543,6 +599,8 @@ div#workEditor {
grid-area: handling; }
#thirdParties {
grid-area: tparties; }
#referrers {
grid-area: referrers; }
#errors {
grid-area: errors; }
@@ -657,5 +715,4 @@ div#workEditor {
}
}
</style>

View File

@@ -5,6 +5,11 @@
<span>{{ evaluation.evaluation.title.fr }}</span>
</div>
<div class="item-url mt-3 mb-4" v-if="evaluation.evaluation.url">
<i class="fa fa-link fa-lg"></i>
<a :href="evaluation.evaluation.url" target="_blank">{{ evaluation.evaluation.url }}</a>
</div>
<div>
<form-evaluation ref="FormEvaluation" :key="evaluation.key" :evaluation="evaluation"></form-evaluation>
@@ -21,8 +26,8 @@
></list-workflow-modal>
</li>
<li>
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a>
<li v-if="canDelete">
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')">{{ $t('delete_evaluation')}}</a>
</li>
</ul>
</div>
@@ -67,7 +72,8 @@ const i18n = {
sure: "Êtes-vous sûr?",
sure_description: "Cette évaluation sera supprimée de cette action d'accompagnement",
ok: "Supprimer"
}
},
delete_evaluation: "Supprimer l'évaluation",
}
}
};
@@ -93,6 +99,19 @@ export default {
pickedEvaluations() {
return this.$store.state.evaluationsPicked;
},
canDelete() {
if (this.evaluation.workflows.length > 0) {
return false;
}
for (let doc of this.evaluation.documents) {
if (doc.workflows.length > 0) {
return false;
}
}
return true;
},
},
methods: {
removeEvaluation(e) {
@@ -128,4 +147,11 @@ export default {
}
}
}
div.item-url {
i {
color: unset!important;
margin-left: 1rem;
margin-right: 0.5rem;
}
}
</style>

View File

@@ -65,16 +65,17 @@
<h5>{{ $t('Documents') }} :</h5>
<div class="flex-table">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.key">
<div class="item-bloc" v-for="(d, i) in evaluation.documents" :key="d.id">
<div class="item-row">
<div class="input-group input-group-lg mb-3">
<div>
<input
class="form-control form-control-lg"
class="form-control"
style="font-weight: bold;"
type="text"
:value="d.title"
:id="d.id"
:data-key="i"
@input="onInputDocumentTitle"/>
</div>
</div>
@@ -84,6 +85,8 @@
<p v-if="d.createdBy" class="createdBy">Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div>
</div>
<div class="item-row">
<div class="item-col">
<ul class="record_actions" >
<li v-if="d.workflows_availables.length > 0">
@@ -99,9 +102,26 @@
></list-workflow-modal>
</li>
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a>
<add-async-upload
:buttonTitle="$t('replace')"
:options="asyncUploadOptions"
:btnClasses="{'btn': true, 'btn-edit': true}"
@addDocument="(arg) => replaceDocument(d, arg)"
>
</add-async-upload>
</li>
<li>
<add-async-upload-downloader
:buttonTitle="$t('download')"
:storedObject="d.storedObject"
>
</add-async-upload-downloader>
</li>
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-wopilink"></a>
</li>
<li v-if="d.workflows.length === 0">
<a class="btn btn-delete" @click="removeDocument(d)">
</a>
</li>
@@ -152,6 +172,7 @@ 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 AddAsyncUploadDownloader from 'ChillDocStoreAssets/vuejs/_components/AddAsyncUploadDownloader.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
@@ -176,7 +197,9 @@ const i18n = {
document_upload: "Téléverser un document",
document_title: "Titre du document",
template_title: "Nom du template",
browse: "Ajouter un document"
browse: "Ajouter un document",
replace: "Remplacer",
download: "Télécharger le fichier existant"
}
}
};
@@ -188,6 +211,7 @@ export default {
ckeditor: CKEditor.component,
PickTemplate,
AddAsyncUpload,
AddAsyncUploadDownloader,
ListWorkflowModal,
},
i18n,
@@ -274,8 +298,9 @@ export default {
},
onInputDocumentTitle(event) {
const id = Number(event.target.id);
const key = Number(event.target.dataset.key) + 1;
const title = event.target.value;
this.$store.commit('updateDocumentTitle', {id: id, evaluationKey: this.evaluation.key, title: title});
this.$store.commit('updateDocumentTitle', {id: id, key: key, evaluationKey: this.evaluation.key, title: title});
},
addDocument(storedObject) {
let document = {
@@ -285,6 +310,14 @@ export default {
};
this.$store.commit('addDocument', {key: this.evaluation.key, document: document});
},
replaceDocument(oldDocument, storedObject) {
let document = {
type: 'accompanying_period_work_evaluation_document',
storedObject: storedObject,
title: oldDocument.title
};
this.$store.commit('replaceDocument', {key: this.evaluation.key, document: document, oldDocument: oldDocument});
},
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});

View File

@@ -31,6 +31,7 @@ const store = createStore({
.map(p => p.person),
handlingThirdParty: window.accompanyingCourseWork.handlingThierParty,
thirdParties: window.accompanyingCourseWork.thirdParties,
referrers: window.accompanyingCourseWork.referrers,
isPosting: false,
errors: [],
},
@@ -54,6 +55,9 @@ const store = createStore({
hasHandlingThirdParty(state) {
return state.handlingThirdParty !== null;
},
hasReferrers(state) {
return state.referrers.length > 0;
},
hasThirdParties(state) {
return state.thirdParties.length > 0;
},
@@ -82,6 +86,7 @@ const store = createStore({
},
results: state.resultsPicked.map(r => ({id: r.id, type: r.type})),
thirdParties: state.thirdParties.map(t => ({id: t.id, type: t.type})),
referrers: state.referrers.map(t => ({id: t.id, type: t.type})),
goals: state.goalsPicked.map(g => {
let o = {
type: g.type,
@@ -131,9 +136,9 @@ 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) => {
documents: e.documents.map((d, docIndex) => {
return Object.assign(d, {
key: index
key: docIndex
});
}),
});
@@ -213,13 +218,26 @@ const store = createStore({
}));
},
removeDocument(state, {key, document}) {
let evaluations = state.evaluationsPicked.find(e => e.key === key);
if (evaluations === undefined) {
let evaluation = state.evaluationsPicked.find(e => e.key === key);
if (evaluation === undefined) {
return;
}
evaluation.documents = evaluation.documents.filter(d => d.key !== document.key);
},
replaceDocument(state, payload) {
let evaluation = state.evaluationsPicked.find(e => e.key === payload.key);
if (evaluation === undefined) {
return;
}
evaluations.documents = evaluations.documents.filter(d => d.key !== document.key);
let newDocument = Object.assign(
payload.document, {
key: evaluation.documents.length + 1,
workflows_availables: state.work.workflows_availables_evaluation_documents,
workflows: [],
}
);
evaluation.documents = evaluation.documents.map(d => d.id === payload.oldDocument.id ? newDocument : d);
},
addEvaluation(state, evaluation) {
let e = {
@@ -302,6 +320,18 @@ const store = createStore({
state.thirdParties = state.thirdParties
.filter(t => t.id !== thirdParty.id);
},
addReferrers(state, referrers) {
let ids = state.referrers.map(t => t.id);
let unexistings = referrers.filter(t => !ids.includes(t.id));
for (let i in unexistings) {
state.referrers.push(unexistings[i]);
}
},
removeReferrer(state, user) {
state.referrers = state.referrers
.filter(u => u.id !== user.id);
},
setErrors(state, errors) {
state.errors = errors;
},
@@ -309,13 +339,17 @@ const store = createStore({
state.isPosting = st;
},
updateDocumentTitle(state, payload) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
if (payload.id === 0) {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.key === payload.key).title = payload.title;
} else {
state.evaluationsPicked.find(e => e.key === payload.evaluationKey)
.documents.find(d => d.id === payload.id).title = payload.title;
}
}
},
actions: {
updateThirdParty({ commit }, payload) {
console.log(payload);
commit('updateThirdParty', payload);
},
getReachablesGoalsForAction({ getters, commit, dispatch }) {
@@ -408,6 +442,9 @@ const store = createStore({
removeDocument({commit}, payload) {
commit('removeDocument', payload);
},
replaceDocument({commit}, payload) {
commit('replaceDocument', payload);
},
submit({ getters, state, commit }, callback) {
let
payload = getters.buildPayload,

View File

@@ -113,7 +113,18 @@
</template>
</modal>
</teleport>
<ul class="record_actions sticky-form-buttons">
<li>
<add-persons
buttonTitle="visgraph.add_person"
modalTitle="visgraph.add_person"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="addNewPersons"
ref="addPersons">
</add-persons>
</li>
</ul>
</template>
<script>
@@ -124,12 +135,14 @@ import VueMultiselect from 'vue-multiselect'
import { getRelationsList, postRelationship, patchRelationship, deleteRelationship } from "./api"
import { splitId, getAge } from "./vis-network"
import { visMessages } from "./i18n";
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
export default {
name: "App",
components: {
Modal,
VueMultiselect
VueMultiselect,
AddPersons
},
props: ['household_id'],
data() {
@@ -159,6 +172,14 @@ export default {
},
canvas: null,
link: null,
addPersons: {
key: 'filiation',
options: {
type: ['person'],
priority: null,
uniq: false,
}
}
}
},
computed: {
@@ -235,12 +256,28 @@ export default {
this.initGraph()
this.listenOnGraph()
this.getRelationsList()
console.log(this.persons);
this.canvas = document.getElementById('visgraph').querySelector('canvas')
this.link = document.getElementById('exportCanvasBtn')
},
methods: {
addNewPersons({ selected, modal }) {
// console.log('@@@ CLICK button addNewPersons', selected);
selected.forEach(function(item) {
this.$store.dispatch('addMorePerson', item.result)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: violations})
}
});
}, this
);
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
initGraph() {
this.container = document.getElementById('visgraph')
// Instanciate vis objects in separate window variables, see vis-network.js

View File

@@ -1,5 +1,11 @@
const visMessages = {
fr: {
add_persons: {
title: "Ajouter des usagers",
suggested_counter: "Pas de résultats | 1 résultat | {count} résultats",
selected_counter: " 1 sélectionné | {count} sélectionnés",
search_some_persons: "Rechercher des personnes..",
},
visgraph: {
Course: 'Parcours',
Household: 'Ménage',
@@ -21,12 +27,48 @@ const visMessages = {
between: "entre",
and: "et",
add_link: "Créer un lien de filiation",
add_person: "Ajouter une personne",
create_link_help: "Pour créer un lien de filiation, cliquez d'abord sur un usager, puis sur un second ; précisez ensuite la nature du lien dans le formulaire d'édition.",
refresh: "Rafraîchir",
screenshot: "Prendre une photo",
choose_relation: "Choisissez le lien de parenté",
relationship_household: "Filiation du ménage",
},
item: {
type_person: "Usager",
type_user: "TMS",
type_thirdparty: "Tiers professionnel",
type_household: "Ménage"
},
person: {
firstname: "Prénom",
lastname: "Nom",
born: (ctx) => {
if (ctx.gender === 'man') {
return 'Né le';
} else if (ctx.gender === 'woman') {
return 'Née le';
} else {
return 'Né·e le';
}
},
center_id: "Identifiant du centre",
center_type: "Type de centre",
center_name: "Territoire", // vendée
phonenumber: "Téléphone",
mobilenumber: "Mobile",
altnames: "Autres noms",
email: "Courriel",
gender: {
title: "Genre",
placeholder: "Choisissez le genre de l'usager",
woman: "Féminin",
man: "Masculin",
neuter: "Neutre, non binaire",
undefined: "Non renseigné"
}
},
error_only_one_person: "Une seule personne peut être sélectionnée !",
edit: 'Éditer',
del: 'Supprimer',
back: 'Revenir en arrière',

View File

@@ -117,10 +117,27 @@ const store = createStore({
return group
},
getPersonById: (state) => (person_id) => {
return state.persons.find(p => p._id === person_id);
}
},
mutations: {
addPerson(state, [person, options]) {
if (!'_id' in person) {
person._id = person.id
person.id = `person_${person.id}`
}
let existing = state.persons.find(p => p._id === person._id);
if (typeof existing !== 'undefined') {
if (!options.folded && person.folded) {
// unfold
}
return;
}
let age = getAge(person)
age = (age === '')? '' : ' - ' + age
@@ -232,6 +249,9 @@ const store = createStore({
//// unfold
unfoldPerson(state, person) {
if (!person.folded) {
return;
}
//console.log('unfoldPerson', person)
person.label = person._label
delete person._label
@@ -261,6 +281,31 @@ const store = createStore({
dispatch('fetchInfoForPerson', person)
},
/**
* Add a person manually
*
* @param commit
* @param dispatch
* @param person
*/
addMorePerson({ commit, dispatch, getters }, person) {
let nodeId = `person_${person.id}`;
if (getters.isPersonLoaded(person.id)) {
if (getters.isExcludedNode(nodeId)) {
commit('removeExcludedNode', nodeId);
let p = getters.getPersonById(person.id);
if (typeof p !== 'undefined') {
commit('unfoldPerson', p);
} else {
throw 'a person loaded was not found';
}
commit('updateHack');
}
} else {
return dispatch('addPerson', person);
}
},
/**
* 2) Fetch infos for this person (hub)
* @param object
@@ -287,7 +332,7 @@ const store = createStore({
//console.log(' isHouseholdLoading ?', getters.isHouseholdLoading(person.current_household_id))
if (! getters.isHouseholdLoading(person.current_household_id)) {
commit('markHouseholdLoading', person.current_household_id)
getHouseholdByPerson(person)
return getHouseholdByPerson(person)
.then(household => new Promise(resolve => {
commit('addHousehold', household)
// DISABLED: in init or expand loop, layer is uncheck when added
@@ -295,7 +340,7 @@ const store = createStore({
//commit('updateHack')
dispatch('addLinkFromPersonsToHousehold', household)
commit('updateHack')
resolve()
resolve();
})
).catch( () => {
commit('unmarkHouseholdLoading', person.current_household_id)
@@ -335,7 +380,7 @@ const store = createStore({
* @param person
*/
fetchCoursesByPerson({ commit, dispatch }, person) {
getCoursesByPerson(person)
return getCoursesByPerson(person)
.then(courses => new Promise(resolve => {
dispatch('addCourses', courses)
resolve()
@@ -383,6 +428,8 @@ const store = createStore({
dispatch('addMissingPerson', [p.person, course])
}
})
return Promise.resolve();
},
/**

View File

@@ -164,14 +164,8 @@ const getGender = (gender) => {
* @returns {string|null}
*/
const getAge = (person) => {
if (person.birthdate) {
let birthdate = new Date(person.birthdate.datetime)
if (person.deathdate) {
let deathdate = new Date(person.deathdate.datetime)
return (deathdate.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
}
let now = new Date()
return (now.getFullYear() - birthdate.getFullYear()) + visMessages.fr.visgraph.years
if (person.age) {
return person.age + ' ' + visMessages.fr.visgraph.years;
}
return ''
}

View File

@@ -1,12 +1,12 @@
<template>
<ul class="list-suggest add-items" v-if="suggested.length > 0">
<li v-for="r in suggested" @click="setReferrer(r)"><span>{{ r.text }}</span></li>
<li v-for="(r, i) in suggested" @click="setReferrer(r)" :key="i"><span>{{ r.text }}</span></li>
</ul>
</template>
<script>
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods.js';
export default {
name: "SetReferrer",

View File

@@ -87,10 +87,14 @@ export default {
currentMembers() {
let members = this.household.members.filter(m => this.household.current_members_id.includes(m.id))
.sort((a, b) => {
if (a.position.ordering < b.position.ordering) {
const orderA = a.position ? a.position.ordering : 0;
const orderB = b.position ? b.position.ordering : 0;
if (orderA < orderB) {
return -1;
}
if (a.position.ordering > b.position.ordering) {
if (orderA > orderB) {
return 1;
}
if (a.holder && !b.holder) {

View File

@@ -28,11 +28,14 @@
{% if w.createdBy %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Referrer'|trans }}</h3>
<h3>{{ 'Referrers'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item">
{{ w.createdBy|chill_entity_render_box }}
{% for u in w.referrers %}
{{ u|chill_entity_render_box }}
{% if not loop.last %}, {% endif %}
{% endfor %}
</p>
</div>
</div>
@@ -130,11 +133,13 @@
href="{{ chill_path_add_return_path('chill_person_accompanying_period_work_edit', { 'id': w.id }) }}"
></a>
</li>
<li>
<a class="btn btn-delete" title="{{ 'Delete'|trans }}"
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}"
></a>
</li>
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE', w) %}
<li>
<a class="btn btn-delete" title="{{ 'Delete'|trans }}"
href="{{ path('chill_person_accompanying_period_work_delete', { 'id': w.id } ) }}"
></a>
</li>
{% endif %}
</ul>
{% endif %}
</div>

View File

@@ -117,12 +117,15 @@
</div>
{% endif %}
<div class="item-row separator">
<div class="item-col item-meta">
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) %}
{% if notif_counter.total > 0 %}
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) }}
{% endif %}
</div>
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) %}
{% if notif_counter.total > 0 %}
<div class="item-col item-meta">
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) }}
</div>
{% endif %}
{% if itemMeta is defined %}
{{ itemMeta }}
{% endif %}
<div class="item-col">
{% if recordAction is defined %}
<ul class="record_actions">

View File

@@ -0,0 +1,82 @@
{% extends 'ChillMainBundle::layout.html.twig' %}
{% block title 'period_by_user_list.Period by user'|trans %}
{% block js %}
{{ encore_entry_script_tags('mod_set_referrer') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('mod_set_referrer') }}
{% endblock %}
{% macro period_meta(period) %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_UPDATE', period) %}
<div class="item-col item-meta">
{% set job_id = null %}
{% if period.job is defined %}
{% set job_id = period.job.id %}
{% endif %}
<span
data-set-referrer-app="data-set-referrer-app"
data-set-referrer-accompanying-period-id="{{ period.id }}"
data-set-referrer-job-id="{{ job_id }}"
></span>
</div>
{% endif %}
{% endmacro %}
{% macro period_actions(period) %}
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_SEE', period) %}
<li>
<a href="{{ chill_path_add_return_path('chill_person_accompanying_course_index', {'accompanying_period_id': period.id}) }}" class="btn btn-show"></a>
</li>
{% endif %}
{% endmacro %}
{% import _self as m %}
{% block content %}
<div class="col-10">
<h1>{{ block('title') }}</h1>
{{ form_start(form) }}
<div class="row filter-box">
<div class="col-md-6">
{{ form_label(form.user ) }}
{{ form_widget(form.user, {'attr': {'class': 'select2'}}) }}
</div>
</div>
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-save change-icon">
<i class="fa fa-filter"></i> Filtrer
</button>
</li>
</ul>
{{ form_end(form) }}
{% if form.user.vars.value is empty %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Pick a user'|trans }}</p>
{% elseif periods|length == 0 and form.user.vars.value is not empty %}
<p class="chill-no-data-statement">{{ 'period_by_user_list.Any course or no authorization to see them'|trans }}</p>
{% else %}
<p><span class="badge rounded-pill bg-primary">{{ paginator.totalItems }}</span> parcours à réassigner (calculé ce jour à {{ null|format_time('medium') }})</p>
<div class="flex-table">
{% for period in periods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period,
'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %}
{% endfor %}
</div>
{% endif %}
{{ chill_pagination(paginator) }}
</div>
{% endblock %}

View File

@@ -167,6 +167,18 @@
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
</li>
{% endif %}
{% if person.email is not empty %}
<li>
<i class="fa fa-li fa-envelope-o"></i><a href="{{ 'mailto:' ~ person.email }}">
{{ person.email }}
</a>
</li>
{% else %}
<li>
<i class="fa fa-li fa-envelope-o"></i>
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
</li>
{% endif %}
{% if options['addCenter'] and person|chill_resolve_center|length > 0 %}
<li>
<i class="fa fa-li fa-long-arrow-right"></i>

View File

@@ -46,13 +46,13 @@
{{ 'Household summary'|trans }}
</a>
</li>
{#
{# TODO: add ACL to check if user is allowed to edit household? #}
<li>
<a href="{{ path ('chill_person_household_accompanying_course_new', {'household_id' : [ household.id ] } ) }}" class="btn btn-create">
{{ 'Create an accompanying period'|trans }}
<a class="btn btn-create"
href="{{ path ('chill_household_accompanying_course_new', {'household_id' : household.id } ) }}" role="button">
{{ 'Create an accompanying period'|trans }}
</a>
</li>
#}
</ul>
</div>

View File

@@ -0,0 +1,26 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% block title 'household_composition.create'|trans %}
{% block block_post_menu %}
<div class="post-menu"></div>
{% endblock %}
{% block content %}
<h1>{{ 'household_composition.Create'|trans }}</h1>
<div>
{{ form_start(form) }}
{{ form_widget(form) }}
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-create">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% endblock %}

View File

@@ -15,7 +15,7 @@
{% else %}
<div class="flex-table">
{% for c in compositions %}
{% if c.id != editId %}
{# {% if c.id != editId %} #}
<div class="item-bloc">
<div class="item-row">
<div class="item-col">
@@ -42,7 +42,7 @@
<div class="item-row">
<ul class="record_actions">
<li>
<a href="{{ path('chill_person_household_composition_index', {'id': c.household.id, 'edit': c.id}) }}" class="btn btn-edit"></a>
<a href="{{ path('chill_person_household_composition_new', {'id': c.household.id, 'edit': c.id}) }}" class="btn btn-edit"></a>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_composition_delete', {'composition_id': c.id,
@@ -54,45 +54,17 @@
</div>
{% endif %}
</div>
{% else %}
{{ form_start(form) }}
{{ form_widget(form) }}
<ul class="record_actions">
<li class="cancel" style="margin-right: auto;">
<a class="btn btn-cancel" href="{{ path('chill_person_household_composition_index', {'id': c.household.id}) }}">{{ 'Cancel'|trans }}</a>
</li>
<li>
<button type="submit" class="btn btn-create">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endif %}
{% endfor %}
</div>
{% endif %}
<div id="collapseForm" class="{% if not isPosted %}collapse{% endif %}">
{{ form_start(form) }}
{{ form_widget(form) }}
<ul class="record_actions">
<li>
<button type="submit" class="btn btn-create">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
</div>
{% if editId == -1 %}
{% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', household) %}
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-primary btn-create change-icon" type="button" data-bs-toggle="collapse" data-bs-target="#collapseForm" aria-expanded="false" aria-controls="collapseForm">
{{ 'Create'|trans }}
</button>
</li>
<li>
<a href="{{ chill_path_add_return_path('chill_person_household_composition_new', {'id': household.id,}) }}"
class="btn btn-new"
title="{{ 'Create'|trans }}">{{ 'Create'|trans }}</a>
</li>
</ul>
{% endif %}
</div>

View File

@@ -58,6 +58,13 @@
}) }}
</span>
{%- endif -%}
{% if person.getCurrentHousehold is not null %}
<span>
<a class="household-link" href="{{ chill_path_add_return_path('chill_person_household_summary', { 'household_id' : person.getCurrentHousehold.id } ) }}">
<i class="fa fa-home"></i>
</a>
</span>
{% endif %}
</div>
</div>

View File

@@ -1,15 +1,25 @@
{% extends "@ChillPerson/Person/layout.html.twig" %}
{% set activeRouteKey = 'chill_person_resource_list' %}
{% block title %}{{ 'Person resources'|trans|capitalize ~ ' ' ~ person|chill_entity_render_string }}{% endblock %}
{% block js %}
{{ encore_entry_script_tags('page_person_resource_showhide_input') }}
{{ encore_entry_script_tags('mod_pickentity_type') }}
{% endblock %}
{% block css %}
{{ encore_entry_link_tags('page_person_resource_showhide_input') }}
{{ encore_entry_link_tags('mod_pickentity_type') }}
{% endblock %}
{% block personcontent %}
<h1 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h1>
<div class="col-md col-xxl">
<div id="collapseForm" class="{% if not form.vars.submitted %}collapse{% endif %}">
<h3 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h3>
{% include "@ChillPerson/PersonResource/form.html.twig" %}
</div>
<ul class="record_actions sticky-form-buttons">
<li>
<button class="btn btn-primary btn-create change-icon" type="button" data-bs-toggle="collapse" data-bs-target="#collapseForm" aria-expanded="false" aria-controls="collapseForm">
{{ 'Add a person resource'|trans }}
</button>
</li>
</ul>
</div>
</div>
{% endblock %}

View File

@@ -85,8 +85,14 @@
<p class="chill-no-data-statement">{{ 'There are no available resources'|trans }}</p>
{% endif %}
<h1 style="margin-bottom: 2rem;">{{ 'Add a person resource'|trans }}</h1>
{% include "@ChillPerson/PersonResource/create.html.twig" %}
{% if is_granted('CHILL_PERSON_UPDATE', person) %}
<ul class="record_actions sticky-form-buttons">
<li>
<a href="{{ chill_path_add_return_path('chill_person_resource_new', {'person_id': person.id,}) }}"
class="btn btn-new"
title="{{ 'Create'|trans }}">{{ 'Create'|trans }}</a>
</li>
</ul>
{% endif %}
{% endblock %}

View File

@@ -49,7 +49,12 @@
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}} </span>
<span class="chill-entity entity-person badge-person">{{ a.hostPerson|chill_entity_render_box }}</span>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: a.hostPerson.id },
buttonText: a.hostPerson|chill_entity_render_string,
isDead: a.hostPerson.deathdate is not null
} %}
</li>
<li>
{% set address_date = date(a.startDate|date("m/d/Y")) %}
@@ -62,7 +67,12 @@
<li>
<i class="fa fa-li fa-home"></i>
<span class="item-key">{{ "Address of"|trans}}</span>
<span class="chill-entity entity-person badge-thirdparty">{{ a.hostThirdParty|chill_entity_render_box }}</span>
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'thirdparty', id: a.hostThirdParty.id },
buttonText: a.hostThirdParty|chill_entity_render_string,
isDead: false,
} %}
</li>
<li>
{% if a.hostThirdParty.address is not null %}

View File

@@ -1,5 +1,10 @@
{%- import "@ChillDocStore/Macro/macro.html.twig" as m -%}
<div class="flex-table accompanying_course_work-list">
<div class="item-bloc evaluation-item bg-chill-llight-gray">
<div class="item-row mb-2">
<h1>{{ doc.title }}</h1>
</div>
<div class="item-row mb-2">
<h2 class="badge-title">
<span class="title_label"></span>
@@ -20,6 +25,24 @@
</span>
</h2>
</div>
<div class="item-row mb-2">
<div class="item-col" style="width: 17%;">
<h4 class="title_label">
{{ 'Participants'|trans }}
</h4>
</div>
<div class="item-col list">
{% for p in evaluation.accompanyingPeriodWork.persons %}
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
targetEntity: { name: 'person', id: p.id },
action: 'show',
displayBadge: true,
buttonText: p|chill_entity_render_string,
isDead: p.deathdate is not null
} %}
{% endfor %}
</div>
</div>
<div class="item-row column">
<table class="obj-res-eval my-3" style="font-size: 110% !important;">
<thead>
@@ -92,12 +115,19 @@
</div>
{% if display_action is defined and display_action == true %}
{# TODO add acl #}
{% if is_granted('CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE', evaluation.accompanyingPeriodWork) %}
<ul class="record_actions">
<li>{{ m.download_button(doc.storedObject, doc.title) }}</li>
{% if chill_document_is_editable(doc.storedObject) %}
<li>
{{ doc.storedObject|chill_document_edit_button }}
</li>
{% endif %}
<li>
<a class="btn btn-show" href="{{ path('chill_person_accompanying_period_work_edit', {'id': evaluation.accompanyingPeriodWork.id}) }}">
{{ 'Show'|trans }}
</a>
</li>
</ul>
{% endif %}
{% endif %}

View File

@@ -24,6 +24,8 @@ class AccompanyingPeriodWorkVoter extends Voter
{
public const CREATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_CREATE';
public const DELETE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_DELETE';
public const SEE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_SEE';
public const UPDATE = 'CHILL_MAIN_ACCOMPANYING_PERIOD_WORK_UPDATE';
@@ -60,6 +62,7 @@ class AccompanyingPeriodWorkVoter extends Voter
case self::CREATE:
case self::UPDATE:
case self::DELETE:
return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $subject->getAccompanyingPeriod());
default:
@@ -86,6 +89,6 @@ class AccompanyingPeriodWorkVoter extends Voter
private function getRoles(): array
{
return [self::SEE, self::CREATE, self::UPDATE];
return [self::SEE, self::CREATE, self::UPDATE, self::DELETE];
}
}

View File

@@ -14,6 +14,7 @@ namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@@ -46,7 +47,10 @@ class AccompanyingPeriodWorkEvaluationNormalizer implements ContextAwareNormaliz
{
$initial = $this->normalizer->normalize($object, $format, array_merge(
$context,
[self::IGNORE_EVALUATION => spl_object_hash($object)]
[self::IGNORE_EVALUATION => spl_object_hash($object)],
[AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => static function ($object, $format, $context) {
return $object->getId();
}]
));
// due to bug: https://api-platform.com/docs/core/serialization/#collection-relation

View File

@@ -23,6 +23,7 @@ use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
@@ -30,6 +31,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
use function array_map;
use function implode;
use function in_array;
use function is_string;
class PersonDocGenNormalizer implements
ContextAwareNormalizerInterface,
@@ -63,6 +66,15 @@ class PersonDocGenNormalizer implements
$dateContext = $context;
$dateContext['docgen:expects'] = DateTimeInterface::class;
$addressContext = array_merge($context, ['docgen:expects' => Address::class]);
$personResourceContext = array_merge($context, [
'docgen:expects' => Person\PersonResource::class,
// we simplify the list of attributes for the embedded persons
AbstractNormalizer::GROUPS => ['docgen:read'],
// when a person reference the same person... take care of circular references
AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => function ($object, $format, $context) {
return $this->normalizer->normalize(null, $format, $context);
},
]);
if (null === $person) {
return $this->normalizeNullValue($format, $context);
@@ -74,6 +86,7 @@ class PersonDocGenNormalizer implements
$data = [
'type' => 'person',
'id' => $person->getId(),
'isNull' => false,
'civility' => $this->normalizer->normalize($person->getCivility(), $format, array_merge($context, ['docgen:expects' => Civility::class])),
'firstName' => $person->getFirstName(),
@@ -103,6 +116,7 @@ class PersonDocGenNormalizer implements
'memo' => $person->getMemo(),
'numberOfChildren' => (string) $person->getNumberOfChildren(),
'address' => $this->normalizer->normalize($person->getCurrentPersonAddress(), $format, $addressContext),
//'resources' => $this->normalizer->normalize($person->getResources(), $format, $personResourceContext),
];
if ($context['docgen:person:with-household'] ?? false) {
@@ -146,12 +160,23 @@ class PersonDocGenNormalizer implements
);
}
private function hasGroup($context, string $group): bool
{
$groups = $context[AbstractNormalizer::GROUPS] ?? [];
if (is_string($groups)) {
$groups = [$groups];
}
return in_array($group, $groups, true);
}
private function normalizeNullValue(string $format, array $context)
{
$normalizer = new NormalizeNullValueHelper($this->normalizer, 'type', 'person');
$attributes = [
'firstName', 'lastName', 'age', 'altNames', 'text',
'id', 'firstName', 'lastName', 'age', 'altNames', 'text',
'civility' => Civility::class,
'birthdate' => DateTimeInterface::class,
'deathdate' => DateTimeInterface::class,
@@ -168,6 +193,8 @@ class PersonDocGenNormalizer implements
$data = $normalizer->normalize($attributes, $format, $context);
//$data['resources'] = [];
if ($context['docgen:person:with-relations'] ?? false) {
$data['relations'] = [];
}

View File

@@ -33,6 +33,7 @@ use Symfony\Component\Serializer\Normalizer\ObjectToPopulateTrait;
use function array_key_exists;
use function count;
use function in_array;
use function is_string;
/**
* Serialize a Person entity.
@@ -56,11 +57,11 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
private ResidentialAddressRepository $residentialAddressRepository;
public function __construct(
ChillEntityRenderExtension $render,
ChillEntityRenderExtension $render, /* TODO: replace by PersonRenderInterface, as sthis is the only one required */
PersonRepository $repository,
CenterResolverManagerInterface $centerResolverManager,
ResidentialAddressRepository $residentialAddressRepository,
PhoneNumberHelperInterface $phoneNumberHelper
PhoneNumberHelperInterface $phoneNumberHelper /* TODO maybe not necessayr any more */
) {
$this->render = $render;
$this->repository = $repository;
@@ -189,6 +190,10 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
public function normalize($person, $format = null, array $context = [])
{
$groups = $context[AbstractNormalizer::GROUPS] ?? [];
if (is_string($groups)) {
$groups = [$groups];
}
$household = $person->getCurrentHousehold();
$currentResidentialAddresses = $this->residentialAddressRepository->findCurrentResidentialAddressByPerson($person);

View File

@@ -179,7 +179,8 @@ class AccompanyingPeriodWorkEvaluationContext implements
$doc = new AccompanyingPeriodWorkEvaluationDocument();
$doc->setStoredObject($storedObject)
->setAccompanyingPeriodWorkEvaluation($entity)
->setTemplate($template);
->setTemplate($template)
->setTitle($this->translatableStringHelper->localize($template->getName()));
$this->em->persist($doc);
}
}

View File

@@ -0,0 +1,179 @@
<?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\Service\DocGenerator;
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
use Chill\DocStoreBundle\Entity\PersonDocument;
use Chill\DocStoreBundle\Entity\StoredObject;
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Entity\Person;
use DateTime;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use function array_key_exists;
class PersonContext implements DocGeneratorContextWithAdminFormInterface
{
private BaseContextData $baseContextData;
private DocumentCategoryRepository $documentCategoryRepository;
private EntityManagerInterface $em;
private NormalizerInterface $normalizer;
private TranslatableStringHelperInterface $translatableStringHelper;
private TranslatorInterface $translator;
public function __construct(
DocumentCategoryRepository $documentCategoryRepository,
NormalizerInterface $normalizer,
TranslatableStringHelperInterface $translatableStringHelper,
EntityManagerInterface $em,
TranslatorInterface $translator,
BaseContextData $baseContextData
) {
$this->documentCategoryRepository = $documentCategoryRepository;
$this->normalizer = $normalizer;
$this->translatableStringHelper = $translatableStringHelper;
$this->em = $em;
$this->baseContextData = $baseContextData;
$this->translator = $translator;
}
public function adminFormReverseTransform(array $data): array
{
if (array_key_exists('category', $data)) {
$data['category'] = [
'idInsideBundle' => $data['category']->getIdInsideBundle(),
'bundleId' => $data['category']->getBundleId(),
];
}
return $data;
}
public function adminFormTransform(array $data): array
{
$r = [
'mainPerson' => $data['mainPerson'] ?? false,
'mainPersonLabel' => $data['mainPersonLabel'] ?? $this->translator->trans('docgen.Main person'),
];
if (array_key_exists('category', $data)) {
$r['category'] = array_key_exists('category', $data) ?
$this->documentCategoryRepository->find($data['category']) : null;
}
return $r;
}
public function buildAdminForm(FormBuilderInterface $builder): void
{
$builder
->add('category', EntityType::class, [
'placeholder' => 'Choose a document category',
'class' => 'ChillDocStoreBundle:DocumentCategory',
'query_builder' => static function (EntityRepository $er) {
return $er->createQueryBuilder('c')
->where('c.documentClass = :docClass')
->setParameter('docClass', PersonDocument::class);
},
'choice_label' => function ($entity = null) {
return $entity ? $this->translatableStringHelper->localize($entity->getName()) : '';
},
]);
}
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
{
if (!$entity instanceof Person) {
throw new UnexpectedTypeException($entity, Person::class);
}
$data = [];
$data = array_merge($data, $this->baseContextData->getData());
$data['person'] = $this->normalizer->normalize($entity, 'docgen', [
'docgen:expects' => Person::class,
'groups' => ['docgen:read', 'docgen:person:with-household', 'docgen:person:with-relations'],
]);
return $data;
}
public function getDescription(): string
{
return 'docgen.A basic context for person';
}
public function getEntityClass(): string
{
return Person::class;
}
public function getFormData(DocGeneratorTemplate $template, $entity): array
{
return [
'person' => $entity,
];
}
public static function getKey(): string
{
return self::class;
}
public function getName(): string
{
return 'docgen.Person basic';
}
public function hasAdminForm(): bool
{
return true;
}
/**
* @param Person $entity
*/
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
{
$doc = new PersonDocument();
$doc->setTemplate($template)
->setTitle($this->translatableStringHelper->localize($template->getName()))
->setDate(new DateTime())
->setDescription($this->translatableStringHelper->localize($template->getName()))
->setPerson($entity)
->setObject($storedObject);
if (array_key_exists('category', $template->getOptions())) {
$doc
->setCategory(
$this->documentCategoryRepository->find(
$template->getOptions()['category']
)
);
}
$this->em->persist($doc);
}
}

View File

@@ -16,6 +16,7 @@ use Chill\PersonBundle\Entity\Household\HouseholdMember;
use Chill\PersonBundle\Entity\Household\Position;
use Chill\PersonBundle\Entity\Person;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -25,17 +26,24 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
final class HouseholdNormalizerTest extends KernelTestCase
{
private EntityManagerInterface $entityManager;
private ?NormalizerInterface $normalizer;
private array $toDelete;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
$this->entityManager = self::$container->get(EntityManagerInterface::class);
}
public function testNormalizationRecursive()
{
$person = new Person();
$person->setFirstName('ok')->setLastName('ok');
$this->entityManager->persist($person);
$member = new HouseholdMember();
$household = new Household();
$position = (new Position())
@@ -44,7 +52,8 @@ final class HouseholdNormalizerTest extends KernelTestCase
$member->setPerson($person)
->setStartDate(new DateTimeImmutable('1 year ago'))
->setEndDate(new DateTimeImmutable('1 month ago'));
->setEndDate(new DateTimeImmutable('1 month ago'))
->setPosition($position);
$household->addMember($member);

View File

@@ -38,6 +38,7 @@ final class PersonDocGenNormalizerTest extends KernelTestCase
use ProphecyTrait;
private const BLANK = [
'id' => '',
'firstName' => '',
'lastName' => '',
'altNames' => '',

View File

@@ -11,7 +11,15 @@ declare(strict_types=1);
namespace Serializer\Normalizer;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Repository\PersonRepository;
use Chill\PersonBundle\Repository\ResidentialAddressRepository;
use Chill\PersonBundle\Serializer\Normalizer\PersonJsonNormalizer;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@@ -22,12 +30,27 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
*/
final class PersonJsonNormalizerTest extends KernelTestCase
{
private NormalizerInterface $normalizer;
use ProphecyTrait;
private PersonJsonNormalizer $normalizer;
protected function setUp(): void
{
self::bootKernel();
$this->normalizer = self::$container->get(NormalizerInterface::class);
$residentialAddressRepository = $this->prophesize(ResidentialAddressRepository::class);
$residentialAddressRepository
->findCurrentResidentialAddressByPerson(Argument::type(Person::class), Argument::any())
->willReturn([]);
$this->normalizer = $this->buildPersonJsonNormalizer(
self::$container->get(ChillEntityRenderExtension::class),
self::$container->get(PersonRepository::class),
self::$container->get(CenterResolverManagerInterface::class),
$residentialAddressRepository->reveal(),
self::$container->get(PhoneNumberHelperInterface::class),
self::$container->get(NormalizerInterface::class)
);
}
public function testNormalization()
@@ -37,4 +60,24 @@ final class PersonJsonNormalizerTest extends KernelTestCase
$this->assertIsArray($result);
}
private function buildPersonJsonNormalizer(
ChillEntityRenderExtension $render,
PersonRepository $repository,
CenterResolverManagerInterface $centerResolverManager,
ResidentialAddressRepository $residentialAddressRepository,
PhoneNumberHelperInterface $phoneNumberHelper,
NormalizerInterface $normalizer
): PersonJsonNormalizer {
$personJsonNormalizer = new PersonJsonNormalizer(
$render,
$repository,
$centerResolverManager,
$residentialAddressRepository,
$phoneNumberHelper
);
$personJsonNormalizer->setNormalizer($normalizer);
return $personJsonNormalizer;
}
}

View File

@@ -37,6 +37,13 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
$this->translator = $translator;
}
public function getDeletionRoles(): array
{
return [
'_',
];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$doc = $this->getRelatedEntity($entityWorkflow);
@@ -54,8 +61,8 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
return $this->translator->trans(
'workflow.Doc for evaluation (n°%eval%)',
['%eval%' => $entityWorkflow->getRelatedEntityId()]
) . ' - ' . $this->translatableStringHelper->localize($doc->getAccompanyingPeriodWorkEvaluation()
->getEvaluation()->getTitle());
) . ' (' . $this->translatableStringHelper->localize($doc->getAccompanyingPeriodWorkEvaluation()
->getEvaluation()->getTitle()) . ') ' . $doc->getTitle();
}
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingPeriodWorkEvaluationDocument
@@ -63,6 +70,18 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
/**
* @param AccompanyingPeriodWorkEvaluationDocument $object
*
* @return array[]
*/
public function getRelatedObjects(object $object): array
{
return [
['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, $object->getId()],
];
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{
return AccompanyingPeriodWorkEvaluationDocumentVoter::SEE;
@@ -84,6 +103,11 @@ class AccompanyingPeriodWorkEvaluationDocumentWorkflowHandler implements EntityW
];
}
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWorkEvaluationDocument;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluationDocument::class;

View File

@@ -15,6 +15,7 @@ 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\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkEvaluationVoter;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -37,6 +38,11 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
$this->translator = $translator;
}
public function getDeletionRoles(): array
{
return ['_'];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
$evaluation = $this->getRelatedEntity($entityWorkflow);
@@ -61,6 +67,21 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
/**
* @param AccompanyingPeriodWorkEvaluation $object
*/
public function getRelatedObjects(object $object): array
{
$relateds = [];
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluation::class, 'entityId' => $object->getId()];
foreach ($object->getDocuments() as $doc) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, 'entityId' => $doc->getId()];
}
return $relateds;
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{
return AccompanyingPeriodWorkEvaluationVoter::SEE;
@@ -79,6 +100,11 @@ class AccompanyingPeriodWorkEvaluationWorkflowHandler implements EntityWorkflowH
];
}
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWorkEvaluation;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWorkEvaluation::class;

View File

@@ -15,7 +15,10 @@ 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\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodWorkVoter;
use Symfony\Contracts\Translation\TranslatorInterface;
class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInterface
@@ -36,6 +39,11 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
$this->translator = $translator;
}
public function getDeletionRoles(): array
{
return [AccompanyingPeriodWorkVoter::DELETE];
}
public function getEntityData(EntityWorkflow $entityWorkflow, array $options = []): array
{
return [
@@ -58,6 +66,25 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
return $this->repository->find($entityWorkflow->getRelatedEntityId());
}
/**
* @param AccompanyingPeriodWork $object
*/
public function getRelatedObjects(object $object): array
{
$relateds = [];
$relateds[] = ['entityClass' => AccompanyingPeriodWork::class, 'entityId' => $object->getId()];
foreach ($object->getAccompanyingPeriodWorkEvaluations() as $evaluation) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluation::class, 'entityId' => $evaluation->getId()];
foreach ($evaluation->getDocuments() as $doc) {
$relateds[] = ['entityClass' => AccompanyingPeriodWorkEvaluationDocument::class, 'entityId' => $doc->getId()];
}
}
return $relateds;
}
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string
{
return null;
@@ -76,6 +103,11 @@ class AccompanyingPeriodWorkWorkflowHandler implements EntityWorkflowHandlerInte
];
}
public function isObjectSupported(object $object): bool
{
return $object instanceof AccompanyingPeriodWork;
}
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool
{
return $entityWorkflow->getRelatedEntityClass() === AccompanyingPeriodWork::class;

View File

@@ -59,6 +59,10 @@ chill_person_resource_list:
path: /{_locale}/person/{person_id}/resources/list
controller: Chill\PersonBundle\Controller\PersonResourceController::listAction
chill_person_resource_new:
path: /{_locale}/person/{person_id}/resources/new
controller: Chill\PersonBundle\Controller\PersonResourceController::newAction
chill_person_resource_edit:
path: /{_locale}/person/{person_id}/resources/{resource_id}/edit
controller: Chill\PersonBundle\Controller\PersonResourceController::editAction
@@ -91,11 +95,6 @@ chill_person_address_update:
chill_person_timeline:
path: /{_locale}/person/{person_id}/timeline
controller: Chill\PersonBundle\Controller\TimelinePersonController::personAction
options:
menus:
person:
order: 60
label: Timeline
chill_person_admin:
path: "/{_locale}/admin/person"

View File

@@ -41,6 +41,11 @@ services:
autowire: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\ReassignAccompanyingPeriodController:
autoconfigure: true
autowire: true
tags: ['controller.service_arguments']
Chill\PersonBundle\Controller\PersonApiController:
arguments:
$authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper'

View File

@@ -12,3 +12,13 @@ services:
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\PersonAltName'
method: 'prePersistAltName'
Chill\PersonBundle\EventListener\AccompanyingPeriodWorkEventListener:
autoconfigure: true
autowire: true
tags:
-
name: 'doctrine.orm.entity_listener'
event: 'prePersist'
entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork'
method: 'prePersistAccompanyingPeriodWork'

View File

@@ -0,0 +1,37 @@
<?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 url to SocialWork Evaluation.
*/
final class Version20220303113855 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_social_work_evaluation DROP url');
}
public function getDescription(): string
{
return 'Add url to SocialWork Evaluation';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_social_work_evaluation ADD url TEXT DEFAULT NULL');
$this->addSql("UPDATE chill_person_social_work_evaluation SET url = CASE WHEN title->>'fr' LIKE 'http%' THEN title->>'fr' ELSE null END;");
}
}

View File

@@ -0,0 +1,42 @@
<?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 referrers to AccompanyingPeriodWork.
*/
final class Version20220310063629 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('DROP TABLE chill_person_accompanying_period_work_referrer');
}
public function getDescription(): string
{
return 'Add referrers to AccompanyingPeriodWork';
}
public function up(Schema $schema): void
{
$this->addSql('CREATE TABLE chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(accompanyingperiodwork_id, user_id))');
$this->addSql('CREATE INDEX IDX_3619F5EBB99F6060 ON chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id)');
$this->addSql('CREATE INDEX IDX_3619F5EBA76ED395 ON chill_person_accompanying_period_work_referrer (user_id)');
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_referrer ADD CONSTRAINT FK_3619F5EBB99F6060 FOREIGN KEY (accompanyingperiodwork_id) REFERENCES chill_person_accompanying_period_work (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('ALTER TABLE chill_person_accompanying_period_work_referrer ADD CONSTRAINT FK_3619F5EBA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('INSERT INTO chill_person_accompanying_period_work_referrer (accompanyingperiodwork_id, user_id)
SELECT id, createdby_id FROM chill_person_accompanying_period_work');
}
}

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;
/**
* Change constraint on pinnedComment in Accompanying period.
*/
final class Version20220310124318 extends AbstractMigration
{
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period DROP CONSTRAINT fk_e260a868b0804e90');
$this->addSql('ALTER TABLE chill_person_accompanying_period ADD CONSTRAINT fk_e260a868b0804e90 FOREIGN KEY (pinnedcomment_id) REFERENCES chill_person_accompanying_period_comment (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function getDescription(): string
{
return 'Change constraint on pinnedComment in Accompanying period';
}
public function up(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_person_accompanying_period DROP CONSTRAINT FK_E260A868B0804E90');
$this->addSql('ALTER TABLE chill_person_accompanying_period ADD CONSTRAINT FK_E260A868B0804E90 FOREIGN KEY (pinnedComment_id) REFERENCES chill_person_accompanying_period_comment (id) ON DELETE SET NULL NOT DEFERRABLE INITIALLY IMMEDIATE');
}
}

View File

@@ -124,8 +124,8 @@ address_country_code: Code pays
'Alreay existing person': 'Dossiers déjà encodés'
'Add the person': 'Ajouter la personne'
'Add the person and create an accompanying period': "Créer la personne & créer une période d'accompagnement"
'Add the person and create a household': "Créer la personne & créer un ménage"
'Add the person and create an accompanying period': "Créer l'usager ET créer une période d'accompagnement"
'Add the person and create a household': "Créer l'usager' ET créer un ménage"
Show person: Voir le dossier de la personne
'Confirm the creation': 'Confirmer la création'
'You will create this person': 'Vous allez créer le dossier suivant'
@@ -194,6 +194,7 @@ No accompanying user: Aucun accompagnant
No data given: Pas d'information
Participants: Personnes impliquées
Create an accompanying course: Créer un parcours
Accompanying courses of users: Parcours des utilisateurs
This accompanying course is still a draft: Ce parcours est encore à l'état brouillon.
Associated peoples: Usagers concernés
Resources: Interlocuteurs privilégiés
@@ -213,6 +214,7 @@ No requestor: Pas de demandeur
No resources: "Pas d'interlocuteurs privilégiés"
Persons associated: Usagers concernés
Referrer: Référent
Referrers: Référents
Some peoples does not belong to any household currently. Add them to an household soon: Certaines personnes n'appartiennent à aucun ménage actuellement. Renseignez leur ménage dès que possible.
Add to household now: Ajouter à un ménage
Any resource for this accompanying course: Aucun interlocuteur privilégié pour ce parcours
@@ -495,6 +497,7 @@ Are you sure you want to remove this composition?: Etes-vous sûr de vouloir sup
Concerns household n°%id%: Concerne le ménage n°%id%
Composition: Composition
Budget: Budget
The composition has been successfully removed.: La composition a été supprimée.
# accompanying course work
Accompanying Course Actions: Actions d'accompagnements
@@ -542,6 +545,8 @@ docgen:
A basic context for accompanying period: Contexte pour les parcours
A context for accompanying period work: Contexte pour les actions d'accompagnement
A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement
Person basic: Personne (basique)
A basic context for person: Contexte pour les personnes
period_notification:
period_designated_subject: Vous êtes référent d'un parcours d'accompagnement
@@ -565,6 +570,7 @@ household_composition:
Currently no composition: Aucune composition familiale renseignée.
Add a composition: Ajouter une composition familiale
Update composition: Modifier la composition familiale
Create: Créér une nouvelle composition familiale
# docgen
Linked evaluations: Évaluations associées
@@ -575,3 +581,8 @@ My accompanying periods in draft: Mes parcours brouillons
workflow:
Doc for evaluation (n°%eval%): Document de l'évaluation n°%eval%
period_by_user_list:
Period by user: Parcours d'accompagnement par utilisateur
Pick a user: Choisissez un utilisateur pour obtenir la liste de ses parcours
Any course or no authorization to see them: Aucun parcours pour ce référent, ou aucun droit pour visualiser les parcours de ce référent.