Merge conflicts fixed

This commit is contained in:
2022-02-04 10:25:46 +01:00
403 changed files with 15524 additions and 2846 deletions

View File

@@ -9,19 +9,20 @@
declare(strict_types=1);
namespace Chill\PersonBundle\AccompanyingPeriod\Workflow;
namespace Chill\PersonBundle\AccompanyingPeriod\Events;
use Chill\MainBundle\Entity\Notification;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\Event\LifecycleEventArgs;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Workflow\Event\EnteredEvent;
use Symfony\Contracts\Translation\TranslatorInterface;
class WorkflowEventSubscriber implements EventSubscriberInterface
class UserRefEventSubscriber implements EventSubscriberInterface
{
private EntityManagerInterface $em;
@@ -55,23 +56,46 @@ class WorkflowEventSubscriber implements EventSubscriberInterface
}
}
public function postUpdate(AccompanyingPeriod $period, LifecycleEventArgs $args): void
{
if ($period->hasPreviousUser()
&& $period->getUser() !== $this->security->getUser()
&& $period->getStep() !== AccompanyingPeriod::STEP_DRAFT
) {
$this->generateNotificationToUser($period);
}
// we are just out of a flush operation. Launch a new one
$this->em->flush();
}
private function generateNotificationToUser(AccompanyingPeriod $period)
{
$notification = new Notification();
$urgentStatement =
$period->isEmergency() ? strtoupper($this->translator->trans('accompanying_period.emergency')) . ' ' : '';
$notification
->setRelatedEntityId($period->getId())
->setRelatedEntityClass(AccompanyingPeriod::class)
->setTitle($urgentStatement . $this->translator->trans('period_notification.period_designated_subject'))
->setMessage($this->engine->render(
'@ChillPerson/Notification/accompanying_course_designation.md.twig',
[
'accompanyingCourse' => $period,
]
))
->addAddressee($period->getUser());
$this->em->persist($notification);
}
private function onPeriodConfirmed(AccompanyingPeriod $period)
{
if ($period->getUser() instanceof User
&& $period->getUser() !== $this->security->getUser()) {
$notification = new Notification();
$notification
->setRelatedEntityId($period->getId())
->setRelatedEntityClass(AccompanyingPeriod::class)
->setTitle($this->translator->trans('period_notification.period_designated_subject'))
->setMessage($this->engine->render(
'@ChillPerson/Notification/accompanying_course_designation.md.twig',
[
'accompanyingCourse' => $period,
]
))
->addAddressee($period->getUser());
$this->em->persist($notification);
$this->generateNotificationToUser($period);
}
}
}

View File

@@ -13,7 +13,9 @@ namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
@@ -23,8 +25,11 @@ use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Privacy\AccompanyingPeriodPrivacyEvent;
use Chill\PersonBundle\Repository\AccompanyingPeriodACLAwareRepository;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use DateInterval;
use DateTimeImmutable;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
@@ -32,13 +37,14 @@ use Symfony\Component\HttpFoundation\JsonResponse;
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\Serializer\Exception\RuntimeException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Component\Validator\ConstraintViolationListInterface;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Symfony\Component\Workflow\Registry;
use function array_values;
use function count;
@@ -46,6 +52,8 @@ final class AccompanyingCourseApiController extends ApiController
{
private AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository;
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private EventDispatcherInterface $eventDispatcher;
private ReferralsSuggestionInterface $referralAvailable;
@@ -55,17 +63,19 @@ final class AccompanyingCourseApiController extends ApiController
private ValidatorInterface $validator;
public function __construct(
EventDispatcherInterface $eventDispatcher,
ValidatorInterface $validator,
Registry $registry,
AccompanyingPeriodRepository $accompanyingPeriodRepository,
AccompanyingPeriodACLAwareRepository $accompanyingPeriodACLAwareRepository,
ReferralsSuggestionInterface $referralAvailable
EventDispatcherInterface $eventDispatcher,
ReferralsSuggestionInterface $referralAvailable,
Registry $registry,
ValidatorInterface $validator
) {
$this->eventDispatcher = $eventDispatcher;
$this->validator = $validator;
$this->registry = $registry;
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository;
$this->eventDispatcher = $eventDispatcher;
$this->referralAvailable = $referralAvailable;
$this->registry = $registry;
$this->validator = $validator;
}
public function commentApi($id, Request $request, string $_format): Response
@@ -99,6 +109,57 @@ final class AccompanyingCourseApiController extends ApiController
]);
}
/**
* @Route("/api/1.0/person/accompanying-course/list/by-recent-attributions")
*/
public function findMyRecentCourseAttribution(Request $request): JsonResponse
{
$this->denyAccessUnlessGranted('ROLE_USER');
$user = $this->getUser();
if (!$user instanceof User) {
throw new AccessDeniedException();
}
$since = (new DateTimeImmutable('now'))->sub(new DateInterval('P15D'));
$total = $this->accompanyingPeriodRepository->countByRecentUserHistory($user, $since);
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->getSerializer()->serialize(new Counter($total), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->getPaginatorFactory()->create($total);
if (0 === $total) {
return new JsonResponse(
$this->getSerializer()->serialize(new Collection([], $paginator), 'json'),
JsonResponse::HTTP_OK,
[],
true
);
}
$courses = $this->accompanyingPeriodRepository->findByRecentUserHistory(
$user,
$since,
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
return new JsonResponse(
$this->getSerializer()->serialize(new Collection($courses, $paginator), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
/**
* @ParamConverter("person", options={"id": "person_id"})
*/

View File

@@ -275,4 +275,34 @@ class AccompanyingCourseController extends Controller
'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"})
*/
public function reOpenAction(AccompanyingPeriod $accompanyingCourse, Request $request): Response
{
$this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingCourse);
if (null === $accompanyingCourse) {
throw $this->createNotFoundException('period not found');
}
$form = $this->createFormBuilder([])->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$accompanyingCourse->reOpen();
$this->getDoctrine()->getManager()->flush();
return $this->redirectToRoute('chill_person_accompanying_course_index', [
'accompanying_period_id' => $accompanyingCourse->getId(),
]);
}
return $this->render('@ChillPerson/AccompanyingCourse/re_open.html.twig', [
'form' => $form->createView(),
'accompanyingCourse' => $accompanyingCourse,
]);
}
}

View File

@@ -12,10 +12,54 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Controller;
use Chill\MainBundle\CRUD\Controller\ApiController;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkRepository;
use DateInterval;
use DateTimeImmutable;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class AccompanyingCourseWorkApiController extends ApiController
{
private AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository;
public function __construct(AccompanyingPeriodWorkRepository $accompanyingPeriodWorkRepository)
{
$this->accompanyingPeriodWorkRepository = $accompanyingPeriodWorkRepository;
}
/**
* @Route("/api/1.0/person/accompanying-period/work/my-near-end")
*/
public function myWorksNearEndDate(Request $request): JsonResponse
{
$since = (new DateTimeImmutable('now'))
->sub(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
$until = (new DateTimeImmutable('now'))
->add(new DateInterval('P' . $request->query->getInt('since', 15) . 'D'));
$total = $this->accompanyingPeriodWorkRepository
->countNearEndDateByUser($this->getUser(), $since, $until);
if ($request->query->getBoolean('countOnly', false)) {
return $this->json(
new Counter($total),
JsonResponse::HTTP_OK,
[],
['groups' => ['read']]
);
}
$paginator = $this->getPaginatorFactory()->create($total);
$works = $this->accompanyingPeriodWorkRepository
->findNearEndDateByUser($this->getUser(), $since, $until, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber());
$collection = new Collection($works, $paginator);
return $this->json($collection, 200, [], ['groups' => ['read']]);
}
protected function getContextForSerialization(string $action, Request $request, string $_format, $entity): array
{
switch ($action) {

View File

@@ -15,11 +15,15 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository;
use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Serializer\Model\Collection;
use Chill\MainBundle\Serializer\Model\Counter;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\SocialWork\Evaluation;
use Chill\PersonBundle\Repository\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationRepository;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
@@ -28,20 +32,28 @@ use function in_array;
class AccompanyingPeriodWorkEvaluationApiController
{
private AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository;
private DocGeneratorTemplateRepository $docGeneratorTemplateRepository;
private PaginatorFactory $paginatorFactory;
private Security $security;
private SerializerInterface $serializer;
public function __construct(
AccompanyingPeriodWorkEvaluationRepository $accompanyingPeriodWorkEvaluationRepository,
DocGeneratorTemplateRepository $docGeneratorTemplateRepository,
SerializerInterface $serializer,
PaginatorFactory $paginatorFactory
PaginatorFactory $paginatorFactory,
Security $security
) {
$this->accompanyingPeriodWorkEvaluationRepository = $accompanyingPeriodWorkEvaluationRepository;
$this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository;
$this->serializer = $serializer;
$this->paginatorFactory = $paginatorFactory;
$this->security = $security;
}
/**
@@ -69,11 +81,46 @@ class AccompanyingPeriodWorkEvaluationApiController
$paginator->setItemsPerPage(count($evaluations));
return new JsonResponse($this->serializer->serialize(
new Collection($evaluations, $paginator),
new Collection(array_values($evaluations), $paginator),
'json',
[
AbstractNormalizer::GROUPS => ['read'],
]
), JsonResponse::HTTP_OK, [], true);
}
/**
* @Route("/api/1.0/person/accompanying-period/work/evaluation/my-near-end")
*/
public function myWorksNearEndDate(Request $request): JsonResponse
{
$total = $this->accompanyingPeriodWorkEvaluationRepository
->countNearMaxDateByUser($this->security->getUser());
if ($request->query->getBoolean('countOnly', false)) {
return new JsonResponse(
$this->serializer->serialize(new Counter($total), 'json', ['groups' => ['read']]),
JsonResponse::HTTP_OK,
[],
true
);
}
$paginator = $this->paginatorFactory->create($total);
$works = $this->accompanyingPeriodWorkEvaluationRepository
->findNearMaxDateByUser(
$this->security->getUser(),
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
$collection = new Collection($works, $paginator);
return new JsonResponse(
$this->serializer->serialize($collection, 'json', ['groups' => ['read', 'read:evaluation:include-work']]),
JsonResponse::HTTP_OK,
[],
true
);
}
}

View File

@@ -0,0 +1,208 @@
<?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\Pagination\PaginatorFactory;
use Chill\PersonBundle\Entity\Household\Household;
use Chill\PersonBundle\Entity\Household\HouseholdComposition;
use Chill\PersonBundle\Form\HouseholdCompositionType;
use Chill\PersonBundle\Repository\Household\HouseholdCompositionRepository;
use Chill\PersonBundle\Repository\Household\HouseholdRepository;
use Chill\PersonBundle\Security\Authorization\HouseholdVoter;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class HouseholdCompositionController extends AbstractController
{
private EngineInterface $engine;
private EntityManagerInterface $entityManager;
private FormFactoryInterface $formFactory;
private HouseholdCompositionRepository $householdCompositionRepository;
private HouseholdRepository $householdRepository;
private PaginatorFactory $paginatorFactory;
private Security $security;
private TranslatorInterface $translator;
private UrlGeneratorInterface $urlGenerator;
public function __construct(
Security $security,
HouseholdCompositionRepository $householdCompositionRepository,
HouseholdRepository $householdRepository,
PaginatorFactory $paginatorFactory,
FormFactoryInterface $formFactory,
EntityManagerInterface $entityManager,
TranslatorInterface $translator,
EngineInterface $engine,
UrlGeneratorInterface $urlGenerator
) {
$this->security = $security;
$this->householdCompositionRepository = $householdCompositionRepository;
$this->paginatorFactory = $paginatorFactory;
$this->formFactory = $formFactory;
$this->entityManager = $entityManager;
$this->translator = $translator;
$this->engine = $engine;
$this->urlGenerator = $urlGenerator;
$this->householdRepository = $householdRepository;
}
/**
* @Route("/{_locale}/person/household/{household_id}/composition/{composition_id}/delete", name="chill_person_household_composition_delete")
*
* @param mixed $household_id
* @param mixed $composition_id
*/
public function deleteAction(Request $request, $household_id, $composition_id): Response
{
$composition = $this->householdCompositionRepository->find($composition_id);
$household = $this->householdRepository->find($household_id);
$this->denyAccessUnlessGranted(HouseholdVoter::EDIT, $household);
if (null === $composition) {
throw $this->createNotFoundException('Unable to find composition entity.');
}
$form = $this->createFormBuilder()
->setAction($this->generateUrl('chill_person_household_composition_delete', [
'composition_id' => $composition_id,
'household_id' => $household_id,
]))
->setMethod('DELETE')
->add('submit', SubmitType::class, ['label' => 'Delete'])
->getForm();
if ($request->getMethod() === Request::METHOD_DELETE) {
$form->handleRequest($request);
if ($form->isValid()) {
$this->entityManager->remove($composition);
$this->entityManager->flush();
$this->addFlash('success', $this->translator
->trans('The composition has been successfully removed.'));
return $this->redirectToRoute('chill_person_household_composition_index', [
'id' => $household_id,
]);
}
}
return $this->render(
'ChillPersonBundle:HouseholdComposition:delete.html.twig',
[
'household' => $household,
'composition' => $composition,
'form' => $form->createView(),
]
);
}
/**
* @Route("/{_locale}/person/household/{id}/composition/index", name="chill_person_household_composition_index")
*/
public function index(Household $household, Request $request): Response
{
if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) {
throw new AccessDeniedException('not allowed to edit an household');
}
$count = $this->householdCompositionRepository->countByHousehold($household);
$paginator = $this->paginatorFactory->create($count);
$compositions = $this->householdCompositionRepository->findByHousehold(
$household,
['startDate' => 'DESC', 'id' => 'DESC'],
$paginator->getItemsPerPage(),
$paginator->getCurrentPageFirstItemNumber()
);
if ($this->security->isGranted(HouseholdVoter::EDIT, $household)) {
$isEdit = $request->query->has('edit');
if ($isEdit) {
$householdCompositions = $household->getCompositions()->filter(static function (HouseholdComposition $composition) use ($request) {
return $composition->getId() === $request->query->getInt('edit');
});
if ($householdCompositions->count() !== 1) {
throw new BadRequestHttpException('could not find the composition with this id associated to the household');
}
$householdComposition = $householdCompositions->first();
} else {
$householdComposition = (new HouseholdComposition())
->setStartDate(new DateTimeImmutable());
}
$form = $this->formFactory->create(HouseholdCompositionType::class, $householdComposition);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
if (!$isEdit) {
$this->entityManager->persist($householdComposition);
$household->addComposition($householdComposition);
}
$this->entityManager->flush();
$request->getSession()->getFlashBag()->add(
'success',
$this->translator->trans('household_composition.Composition added')
);
return new RedirectResponse(
$this->urlGenerator->generate('chill_person_household_composition_index', [
'id' => $household->getId(),
])
);
}
if ($form->isSubmitted() && !$form->isValid()) {
$request->getSession()->getFlashBag()->add(
'warning',
$this->translator->trans('This form contains errors')
);
}
}
return new Response($this->engine->render(
'@ChillPerson/HouseholdComposition/index.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

@@ -0,0 +1,36 @@
<?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\CRUD\Controller\ApiController;
use Doctrine\ORM\QueryBuilder;
use Symfony\Component\HttpFoundation\Request;
use UnexpectedValueException;
class HouseholdCompositionTypeApiController extends ApiController
{
/**
* @param QueryBuilder $query
*/
protected function customizeQuery(string $action, Request $request, $query): void
{
switch ($action) {
case '_index':
$query->andWhere($query->expr()->eq('e.active', "'TRUE'"));
break;
default:
throw new UnexpectedValueException('unexepcted action: ' . $action);
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,51 @@
<?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\Pagination\PaginatorFactory;
use Chill\PersonBundle\Repository\AccompanyingPeriodRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class UserAccompanyingPeriodController extends AbstractController
{
private AccompanyingPeriodRepository $accompanyingPeriodRepository;
private PaginatorFactory $paginatorFactory;
public function __construct(AccompanyingPeriodRepository $accompanyingPeriodRepository, PaginatorFactory $paginatorFactory)
{
$this->accompanyingPeriodRepository = $accompanyingPeriodRepository;
$this->paginatorFactory = $paginatorFactory;
}
/**
* @Route("/{_locale}/accompanying-periods", name="chill_person_accompanying_period_user")
*/
public function listAction(Request $request)
{
$total = $this->accompanyingPeriodRepository->countBy(['user' => $this->getUser()]);
$pagination = $this->paginatorFactory->create($total);
$accompanyingPeriods = $this->accompanyingPeriodRepository->findBy(
['user' => $this->getUser()],
['openingDate' => 'DESC'],
$pagination->getItemsPerPage(),
$pagination->getCurrentPageFirstItemNumber()
);
return $this->render('@ChillPerson/AccompanyingPeriod/user_periods_list.html.twig', [
'accompanyingPeriods' => $accompanyingPeriods,
'pagination' => $pagination,
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\DataFixtures\ORM;
use Chill\PersonBundle\Entity\Household\HouseholdCompositionType;
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Persistence\ObjectManager;
class LoadHouseholdCompositionType extends AbstractFixture implements FixtureGroupInterface
{
public const TYPES = [
['fr' => 'Couple avec enfant(s)'],
['fr' => 'Couple sans enfant'],
['fr' => 'Mère seule'],
['fr' => 'Père seul'],
['fr' => 'Mère isolée'],
['fr' => 'Père isolé'],
['fr' => 'Homme seul'],
['fr' => 'Femme seule'],
];
public static function getGroups(): array
{
return ['composition-type'];
}
public function load(ObjectManager $manager)
{
foreach (self::TYPES as $type) {
$manager->persist(
(new HouseholdCompositionType())
->setLabel($type)
);
}
$manager->flush();
}
}

View File

@@ -13,6 +13,7 @@ namespace Chill\PersonBundle\DependencyInjection;
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\AccompanyingPeriodResourceVoter;
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
@@ -760,6 +761,21 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac
],
],
],
[
'class' => \Chill\PersonBundle\Entity\Household\HouseholdCompositionType::class,
'name' => 'household_composition',
'base_path' => '/api/1.0/person/houehold/composition/type',
'base_role' => 'ROLE_USER',
'controller' => HouseholdCompositionTypeApiController::class,
'actions' => [
'_index' => [
'methods' => [
Request::METHOD_GET => true,
Request::METHOD_HEAD => true,
],
],
],
],
],
]);
}

View File

@@ -20,11 +20,13 @@ use Chill\MainBundle\Entity\HasScopesInterface;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Entity\UserJob;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Origin;
use Chill\PersonBundle\Entity\AccompanyingPeriod\Resource;
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
use Chill\PersonBundle\Entity\SocialWork\SocialIssue;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\AccompanyingPeriodValidity;
use Chill\PersonBundle\Validator\Constraints\AccompanyingPeriod\ParticipationOverlap;
@@ -44,7 +46,6 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\GroupSequenceProviderInterface;
use UnexpectedValueException;
use function count;
use function in_array;
use const SORT_REGULAR;
@@ -132,7 +133,11 @@ class AccompanyingPeriod implements
* @ORM\Column(type="date", nullable=true)
* @Groups({"read", "write", "docgen:read"})
* @Assert\NotBlank(groups={AccompanyingPeriod::STEP_CLOSED})
* @Assert\GreaterThan(propertyPath="openingDate", groups={AccompanyingPeriod::STEP_CLOSED})
* @Assert\GreaterThanOrEqual(
* propertyPath="openingDate",
* groups={AccompanyingPeriod::STEP_CLOSED},
* message="The closing date must be later than the date of creation"
* )
*/
private ?DateTime $closingDate = null;
@@ -198,11 +203,22 @@ class AccompanyingPeriod implements
*/
private $intensity = self::INTENSITY_OCCASIONAL;
/**
* @ORM\ManyToOne(
* targetEntity=UserJob::class
* )
* @Groups({"read", "write"})
* @Assert\NotBlank(groups={AccompanyingPeriod::STEP_CONFIRMED})
*/
private ?UserJob $job = null;
/**
* @var DateTime
*
* @ORM\Column(type="date")
* @Groups({"read", "write", "docgen:read"})
* @Assert\LessThan(value="tomorrow", groups={AccompanyingPeriod::STEP_CONFIRMED})
* @Assert\LessThanOrEqual(propertyPath="closingDate", groups={AccompanyingPeriod::STEP_CONFIRMED})
*/
private ?DateTime $openingDate = null;
@@ -327,6 +343,21 @@ class AccompanyingPeriod implements
*/
private ?User $user = null;
/**
* @ORM\OneToMany(targetEntity=UserHistory::class, mappedBy="accompanyingPeriod", orphanRemoval=true,
* cascade={"persist", "remove"})
*
* @var Collection|UserHistory[]
*/
private Collection $userHistories;
/**
* Temporary field, which is filled when the user is changed.
*
* Used internally for listener when user change
*/
private ?User $userPrevious = null;
/**
* @ORM\OneToMany(
* targetEntity=AccompanyingPeriodWork::class,
@@ -352,6 +383,7 @@ class AccompanyingPeriod implements
$this->comments = new ArrayCollection();
$this->works = new ArrayCollection();
$this->resources = new ArrayCollection();
$this->userHistories = new ArrayCollection();
}
/**
@@ -528,17 +560,17 @@ class AccompanyingPeriod implements
public function getAvailablePersonLocation(): Collection
{
return $this->getOpenParticipations()
->filter(static function (AccompanyingPeriodParticipation $p) {
return $p->getPerson()->hasCurrentHouseholdAddress();
})
->map(static function (AccompanyingPeriodParticipation $p) {
return $p->getPerson();
});
->filter(
static fn (AccompanyingPeriodParticipation $p): bool => $p->getPerson()->hasCurrentHouseholdAddress()
)
->map(
static fn (AccompanyingPeriodParticipation $p): ?Person => $p->getPerson()
);
}
public function getCenter(): ?Center
{
if (count($this->getPersons()) === 0) {
if ($this->getPersons()->count() === 0) {
return null;
}
@@ -579,9 +611,13 @@ class AccompanyingPeriod implements
*/
public function getComments(): Collection
{
return $this->comments->filter(function (Comment $c) {
return $c !== $this->pinnedComment;
});
$pinnedComment = $this->pinnedComment;
return $this
->comments
->filter(
static fn (Comment $c): bool => $c !== $pinnedComment
);
}
public function getCreatedAt(): ?DateTime
@@ -612,7 +648,7 @@ class AccompanyingPeriod implements
return [[self::STEP_DRAFT, self::STEP_CONFIRMED]];
}
throw new LogicException('no validation group permitted with this step');
throw new LogicException('no validation group permitted with this step: ' . $this->getStep());
}
public function getId(): ?int
@@ -625,6 +661,11 @@ class AccompanyingPeriod implements
return $this->intensity;
}
public function getJob(): ?UserJob
{
return $this->job;
}
/**
* Get the location, taking precedence into account.
*
@@ -717,9 +758,7 @@ class AccompanyingPeriod implements
return $this
->getParticipations()
->filter(
static function (AccompanyingPeriodParticipation $participation) use ($person): bool {
return $participation->getPerson() === $person;
}
static fn (AccompanyingPeriodParticipation $participation): bool => $participation->getPerson() === $person
);
}
@@ -741,9 +780,7 @@ class AccompanyingPeriod implements
return $this
->participations
->map(
static function (AccompanyingPeriodParticipation $participation): Person {
return $participation->getPerson();
}
static fn (AccompanyingPeriodParticipation $participation): ?Person => $participation->getPerson()
);
}
@@ -755,6 +792,11 @@ class AccompanyingPeriod implements
return $this->pinnedComment;
}
public function getPreviousUser(): ?User
{
return $this->userPrevious;
}
/**
* @return Collection|SocialAction[] All the descendant social actions of all
* the descendants of the entity
@@ -868,6 +910,11 @@ class AccompanyingPeriod implements
return $this->works;
}
public function hasPreviousUser(): bool
{
return null !== $this->userPrevious;
}
/**
* Returns true if the closing date is after the opening date.
*/
@@ -981,6 +1028,7 @@ class AccompanyingPeriod implements
{
$this->setClosingDate(null);
$this->setClosingMotive(null);
$this->setStep(AccompanyingPeriod::STEP_CONFIRMED);
}
/**
@@ -1058,6 +1106,13 @@ class AccompanyingPeriod implements
return $this;
}
public function setJob(?UserJob $job): self
{
$this->job = $job;
return $this;
}
/**
* Set openingDate.
*
@@ -1099,6 +1154,9 @@ class AccompanyingPeriod implements
}
if ($comment instanceof Comment) {
if (null !== $this->pinnedComment) {
$this->addComment($this->pinnedComment);
}
$this->addComment($comment);
}
@@ -1170,8 +1228,22 @@ class AccompanyingPeriod implements
return $this;
}
public function setUser(User $user): self
public function setUser(?User $user): self
{
if ($this->user !== $user) {
$this->userPrevious = $this->user;
foreach ($this->userHistories as $history) {
if (null === $history->getEndDate()) {
$history->setEndDate(new DateTimeImmutable('now'));
}
}
if (null !== $user) {
$this->userHistories->add(new UserHistory($this, $user));
}
}
$this->user = $user;
return $this;

View File

@@ -44,7 +44,8 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
{
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class)
* @Serializer\Groups({"read"})
* @Serializer\Groups({"read","read:accompanyingPeriodWork:light"})
* @Serializer\Context(normalizationContext={"groups"={"read"}}, groups={"read:accompanyingPeriodWork:light"})
*/
private ?AccompanyingPeriod $accompanyingPeriod = null;
@@ -63,26 +64,26 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @ORM\Column(type="datetime_immutable")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/
private ?DateTimeImmutable $createdAt = null;
/**
* @ORM\Column(type="boolean")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/
private bool $createdAutomatically = false;
/**
* @ORM\Column(type="text")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/
private string $createdAutomaticallyReason = '';
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/
private ?User $createdBy = null;
@@ -90,7 +91,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Assert\GreaterThan(propertyPath="startDate",
* message="accompanying_course_work.The endDate should be greater than the start date"
* )
@@ -122,7 +123,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light", "read:evaluation:include-work"})
*/
private ?int $id = null;
@@ -135,7 +136,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @ORM\ManyToMany(targetEntity=Person::class)
* @ORM\JoinTable(name="chill_person_accompanying_period_work_person")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"accompanying_period_work:create"})
*/
@@ -151,8 +152,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
/**
* @ORM\ManyToOne(targetEntity=SocialAction::class)
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
* @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Context(normalizationContext={"groups": {"read"}}, groups={"read:accompanyingPeriodWork:light"})
*/
private ?SocialAction $socialAction = null;
@@ -160,7 +162,7 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
* @ORM\Column(type="date_immutable")
* @Serializer\Groups({"accompanying_period_work:create"})
* @Serializer\Groups({"accompanying_period_work:edit"})
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Groups({"read", "docgen:read", "read:accompanyingPeriodWork:light"})
*/
private ?DateTimeImmutable $startDate = null;

View File

@@ -39,6 +39,8 @@ class AccompanyingPeriodWorkEvaluation implements TrackCreationInterface, TrackU
* targetEntity=AccompanyingPeriodWork::class,
* inversedBy="accompanyingPeriodWorkEvaluations"
* )
* @Serializer\Groups({"read:evaluation:include-work"})
* @Serializer\Context(normalizationContext={"groups": {"read:accompanyingPeriodWork:light"}}, groups={"read:evaluation:include-work"})
*/
private ?AccompanyingPeriodWork $accompanyingPeriodWork = null;

View File

@@ -0,0 +1,96 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("chill_person_accompanying_period_user_history")
*/
class UserHistory implements TrackCreationInterface
{
use TrackCreationTrait;
/**
* @ORM\ManyToOne(targetEntity=AccompanyingPeriod::class, inversedBy="userHistories")
* @ORM\JoinColumn(nullable=false)
*/
private ?AccompanyingPeriod $accompanyingPeriod;
/**
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": null})
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\Id
* @ORM\Column(name="id", type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private ?int $id = null;
/**
* @ORM\Column(type="datetime_immutable", nullable=false)
*/
private DateTimeImmutable $startDate;
/**
* @ORM\ManyToOne(targetEntity=User::class)
* @ORM\JoinColumn(nullable=false)
*/
private User $user;
public function __construct(AccompanyingPeriod $accompanyingPeriod, User $user, ?DateTimeImmutable $startDate = null)
{
$this->startDate = $startDate ?? new DateTimeImmutable('now');
$this->accompanyingPeriod = $accompanyingPeriod;
$this->user = $user;
}
public function getAccompanyingPeriod(): AccompanyingPeriod
{
return $this->accompanyingPeriod;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getId(): ?int
{
return $this->id;
}
public function getStartDate(): DateTimeImmutable
{
return $this->startDate;
}
public function getUser(): User
{
return $this->user;
}
public function setEndDate(?DateTimeImmutable $endDate): UserHistory
{
$this->endDate = $endDate;
return $this;
}
}

View File

@@ -11,6 +11,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Household;
use ArrayIterator;
use Chill\MainBundle\Entity\Address;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder;
@@ -23,8 +24,8 @@ use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use function count;
/**
@@ -56,6 +57,18 @@ class Household
*/
private CommentEmbeddable $commentMembers;
/**
* @ORM\OneToMany(
* targetEntity=HouseholdComposition::class,
* mappedBy="household",
* orphanRemoval=true,
* cascade={"persist"}
* )
* @ORM\OrderBy({"startDate": "DESC"})
* @Assert\Valid(traverse=true, groups={"household_composition"})
*/
private Collection $compositions;
/**
* @ORM\Id
* @ORM\GeneratedValue
@@ -90,6 +103,7 @@ class Household
$this->addresses = new ArrayCollection();
$this->members = new ArrayCollection();
$this->commentMembers = new CommentEmbeddable();
$this->compositions = new ArrayCollection();
}
/**
@@ -108,6 +122,18 @@ class Household
return $this;
}
public function addComposition(HouseholdComposition $composition): self
{
if (!$this->compositions->contains($composition)) {
$composition->setHousehold($this);
$this->compositions[] = $composition;
}
$this->householdCompositionConsistency();
return $this;
}
public function addMember(HouseholdMember $member): self
{
if (!$this->members->contains($member)) {
@@ -136,6 +162,14 @@ class Household
return $this->commentMembers;
}
/**
* @return ArrayCollection|Collection|HouseholdComposition[]
*/
public function getCompositions(): Collection
{
return $this->compositions;
}
/**
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\SerializedName("current_address")
@@ -157,6 +191,31 @@ class Household
return null;
}
public function getCurrentComposition(?DateTimeImmutable $at = null): ?HouseholdComposition
{
$at ??= new DateTimeImmutable('today');
$criteria = new Criteria();
$expr = Criteria::expr();
$criteria->where(
$expr->andX(
$expr->orX(
$expr->isNull('endDate'),
$expr->gt('endDate', $at)
),
$expr->lte('startDate', $at)
)
);
$compositions = $this->compositions->matching($criteria);
if ($compositions->count() > 0) {
return $compositions->first();
}
return null;
}
/**
* @Serializer\Groups({"docgen:read"})
*/
@@ -369,11 +428,54 @@ class Household
return $this->waitingForBirthDate;
}
/**
* @internal
*/
public function householdCompositionConsistency(): void
{
$compositionOrdered = $this->compositions->toArray();
usort(
$compositionOrdered,
static function (HouseholdComposition $a, HouseholdComposition $b) {
return $a->getStartDate() <=> $b->getStartDate();
}
);
$iterator = new ArrayIterator($compositionOrdered);
$iterator->rewind();
/** @var ?HouseholdComposition $previous */
$previous = null;
do {
/** @var ?HouseholdComposition $current */
$current = $iterator->current();
if (null !== $previous) {
if (null === $previous->getEndDate() || $previous->getEndDate() > $current->getStartDate()) {
$previous->setEndDate($current->getStartDate());
}
}
$previous = $current;
$iterator->next();
} while ($iterator->valid());
}
public function removeAddress(Address $address)
{
$this->addresses->removeElement($address);
}
public function removeComposition(HouseholdComposition $composition): self
{
if ($this->compositions->removeElement($composition)) {
$composition->setHousehold(null);
}
return $this;
}
public function removeMember(HouseholdMember $member): self
{
if ($this->members->removeElement($member)) {

View File

@@ -0,0 +1,172 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Household;
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
use DateTimeImmutable;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Validator\Constraints as Assert;
/**
* @ORM\Entity
* @ORM\Table(
* name="chill_person_household_composition"
* )
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household_composition_type": HouseholdCompositionType::class
* })
*/
class HouseholdComposition implements TrackCreationInterface, TrackUpdateInterface
{
use TrackCreationTrait;
use TrackUpdateTrait;
/**
* @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_")
*/
private CommentEmbeddable $comment;
/**
* @ORM\Column(type="date_immutable", nullable=true, options={"default": null})
* @Assert\GreaterThanOrEqual(propertyPath="startDate", groups={"Default", "household_composition"})
*/
private ?DateTimeImmutable $endDate = null;
/**
* @ORM\ManyToOne(targetEntity=Household::class, inversedBy="compositions")
* @ORM\JoinColumn(nullable=false)
*/
private ?Household $household = null;
/**
* @ORM\ManyToOne(targetEntity=HouseholdCompositionType::class)
* @ORM\JoinColumn(nullable=false)
*/
private ?HouseholdCompositionType $householdCompositionType = null;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read", "docgen:read"})
*/
private ?int $id = null;
/**
* @ORM\Column(type="integer", nullable=true, options={"default": null})
* @Assert\NotNull
* @Assert\GreaterThanOrEqual(0, groups={"Default", "household_composition"})
*/
private ?int $numberOfChildren = null;
/**
* @ORM\Column(type="date_immutable", nullable=false)
* @Assert\NotNull(groups={"Default", "household_composition"})
*/
private ?DateTimeImmutable $startDate = null;
public function __construct()
{
$this->comment = new CommentEmbeddable();
}
public function getComment(): CommentEmbeddable
{
return $this->comment;
}
public function getEndDate(): ?DateTimeImmutable
{
return $this->endDate;
}
public function getHousehold(): ?Household
{
return $this->household;
}
public function getHouseholdCompositionType(): ?HouseholdCompositionType
{
return $this->householdCompositionType;
}
public function getId(): ?int
{
return $this->id;
}
public function getNumberOfChildren(): ?int
{
return $this->numberOfChildren;
}
public function getStartDate(): ?DateTimeImmutable
{
return $this->startDate;
}
public function setComment(CommentEmbeddable $comment): HouseholdComposition
{
$this->comment = $comment;
return $this;
}
public function setEndDate(?DateTimeImmutable $endDate): HouseholdComposition
{
$this->endDate = $endDate;
if (null !== $this->household) {
$this->household->householdCompositionConsistency();
}
return $this;
}
public function setHousehold(?Household $household): HouseholdComposition
{
$this->household = $household;
return $this;
}
public function setHouseholdCompositionType(?HouseholdCompositionType $householdCompositionType): HouseholdComposition
{
$this->householdCompositionType = $householdCompositionType;
return $this;
}
public function setNumberOfChildren(?int $numberOfChildren): HouseholdComposition
{
$this->numberOfChildren = $numberOfChildren;
return $this;
}
public function setStartDate(?DateTimeImmutable $startDate): HouseholdComposition
{
$this->startDate = $startDate;
if (null !== $this->household) {
$this->household->householdCompositionConsistency();
}
return $this;
}
}

View File

@@ -0,0 +1,76 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Entity\Household;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Entity
* @ORM\Table(
* name="chill_person_household_composition_type"
* )
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
* "household_composition_type": HouseholdCompositionType::class
* })
*/
class HouseholdCompositionType
{
/**
* @ORM\Column(type="boolean")
*/
private bool $active = true;
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @Serializer\Groups({"read", "docgen:read"})
*/
private ?int $id = null;
/**
* @ORM\Column(type="json")
* @Serializer\Groups({"read", "docgen:read"})
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
*/
private array $label = [];
public function getId(): ?int
{
return $this->id;
}
public function getLabel(): array
{
return $this->label;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): HouseholdCompositionType
{
$this->active = $active;
return $this;
}
public function setLabel(array $label): HouseholdCompositionType
{
$this->label = $label;
return $this;
}
}

View File

@@ -151,7 +151,6 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var DateTime
*
* @ORM\Column(type="date", nullable=true)
* @Assert\Date
* @Birthdate
*/
private $birthdate;
@@ -259,7 +258,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank
* @Assert\NotBlank(message="The firstname cannot be empty")
* @Assert\Length(
* max=255,
* )
@@ -282,7 +281,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=9, nullable=true)
* @Assert\NotNull
* @Assert\NotNull(message="The gender must be set")
*/
private $gender;
@@ -326,7 +325,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* @var string
*
* @ORM\Column(type="string", length=255)
* @Assert\NotBlank
* @Assert\NotBlank(message="The lastname cannot be empty")
* @Assert\Length(
* max=255,
* )
@@ -688,7 +687,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
$result = new ArrayCollection();
if ($asParticipantOpen) {
foreach ($this->getOpenedParticipations()
foreach ($this->getAccompanyingPeriodParticipations()
->map(fn (AccompanyingPeriodParticipation $app) => $app->getAccompanyingPeriod())
as $period
) {
@@ -785,12 +784,18 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
* If the `$at` parameter is now, use the method `getCurrentPersonAddress`, which is optimized
* on database side.
*
* @deprecated since chill2.0, address is linked to the household. Use @see{Person::getCurrentHouseholdAddress}
*
* @throws Exception
*/
public function getAddressAt(?DateTime $at = null): ?Address
public function getAddressAt(?DateTimeInterface $at = null): ?Address
{
$at ??= new DateTime('now');
if ($at instanceof DateTimeImmutable) {
$at = DateTime::createFromImmutable($at);
}
/** @var ArrayIterator $addressesIterator */
$addressesIterator = $this->getAddresses()
->filter(static fn (Address $address): bool => $address->getValidFrom() <= $at)
@@ -951,6 +956,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
: null;
}
/**
* Get the household address at the given date.
*
* if the given date is 'now', use instead @see{getCurrentPersonAddress}, which is optimized on
* database side.
*/
public function getCurrentHouseholdAddress(?DateTimeImmutable $at = null): ?Address
{
if (
@@ -1143,7 +1154,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
->where(
$expr->eq('shareHousehold', true)
)
->orderBy(['startDate' => Criteria::DESC]);
->orderBy(['startDate' => Criteria::DESC, 'id' => Criteria::DESC]);
return $this->getHouseholdParticipations()
->matching($criteria);

View File

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

View File

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

View File

@@ -22,6 +22,8 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -52,11 +54,20 @@ final class CreationPersonType extends AbstractType
$builder
->add('firstName')
->add('lastName')
->add('gender', GenderType::class, [
'required' => true, 'placeholder' => null,
])
->add('birthdate', ChillDateType::class, [
'required' => false,
])
->add('gender', GenderType::class, [
'required' => true, 'placeholder' => null,
->add('phonenumber', TelType::class, [
'required' => false,
])
->add('mobilenumber', TelType::class, [
'required' => false,
])
->add('email', EmailType::class, [
'required' => false,
]);
if ($this->askCenters) {

View File

@@ -0,0 +1,60 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Repository\Household\HouseholdCompositionTypeRepository;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
class HouseholdCompositionType extends AbstractType
{
private HouseholdCompositionTypeRepository $householdCompositionTypeRepository;
private TranslatableStringHelperInterface $translatableStringHelper;
public function __construct(HouseholdCompositionTypeRepository $householdCompositionTypeRepository, TranslatableStringHelperInterface $translatableStringHelper)
{
$this->householdCompositionTypeRepository = $householdCompositionTypeRepository;
$this->translatableStringHelper = $translatableStringHelper;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$types = $this->householdCompositionTypeRepository->findAllActive();
$builder
->add('householdCompositionType', EntityType::class, [
'class' => \Chill\PersonBundle\Entity\Household\HouseholdCompositionType::class,
'choices' => $types,
'choice_label' => function (\Chill\PersonBundle\Entity\Household\HouseholdCompositionType $type) {
return $this->translatableStringHelper->localize($type->getLabel());
},
'label' => 'household_composition.Household composition',
])
->add('startDate', ChillDateType::class, [
'required' => true,
'input' => 'datetime_immutable',
])
->add('numberOfChildren', IntegerType::class, [
'required' => true,
'label' => 'household_composition.numberOfChildren',
])
->add('comment', CommentType::class, [
'required' => false,
]);
}
}

View File

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

View File

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

View File

@@ -29,6 +29,8 @@ class MembersEditor
{
public const VALIDATION_GROUP_AFFECTED = 'household_memberships';
public const VALIDATION_GROUP_COMPOSITION = 'household_composition';
public const VALIDATION_GROUP_CREATED = 'household_memberships_created';
private ?Household $household = null;
@@ -77,6 +79,15 @@ class MembersEditor
$this->oldMembershipsHashes[] = spl_object_hash($participation);
}
}
foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) {
if ($participation->getHousehold() === $this->household
&& $participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate()
&& $participation->getStartDate() <= $membership->getStartDate()
) {
$participation->setEndDate($membership->getStartDate());
}
}
}
$this->membershipsAffected[] = $membership;
@@ -129,7 +140,7 @@ class MembersEditor
{
if ($this->hasHousehold()) {
$list = $this->validator
->validate($this->getHousehold(), null, [self::VALIDATION_GROUP_AFFECTED]);
->validate($this->getHousehold(), null, [self::VALIDATION_GROUP_AFFECTED, self::VALIDATION_GROUP_COMPOSITION]);
} else {
$list = new ConstraintViolationList();
}

View File

@@ -82,6 +82,15 @@ class AccompanyingCourseMenuBuilder implements LocalMenuBuilderInterface
$workflow = $this->registry->get($period, 'accompanying_period_lifecycle');
if (null !== $period->getClosingDate()) {
$menu->addChild($this->translator->trans('Re-open accompanying course'), [
'route' => 'chill_person_accompanying_course_reopen',
'routeParameters' => [
'accompanying_period_id' => $period->getId(),
], ])
->setExtras(['order' => 99998]);
}
if ($workflow->can($period, 'close')) {
$menu->addChild($this->translator->trans('Close Accompanying Course'), [
'route' => 'chill_person_accompanying_course_close',

View File

@@ -29,6 +29,7 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
public function buildMenu($menuId, MenuItem $menu, array $parameters): void
{
/** @var \Chill\PersonBundle\Entity\Household\Household $household */
$household = $parameters['household'];
$menu->addChild($this->translator->trans('household.Household summary'), [
@@ -38,6 +39,20 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
], ])
->setExtras(['order' => 10]);
$menu->addChild($this->translator->trans('household.Relationship'), [
'route' => 'chill_person_household_relationship',
'routeParameters' => [
'household_id' => $household->getId(),
], ])
->setExtras(['order' => 15]);
$menu->addChild($this->translator->trans('household_composition.Compositions'), [
'route' => 'chill_person_household_composition_index',
'routeParameters' => [
'id' => $household->getId(),
], ])
->setExtras(['order' => 17]);
$menu->addChild($this->translator->trans('household.Accompanying period'), [
'route' => 'chill_person_household_accompanying_period',
'routeParameters' => [
@@ -51,13 +66,6 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface
'household_id' => $household->getId(),
], ])
->setExtras(['order' => 30]);
$menu->addChild($this->translator->trans('household.Relationship'), [
'route' => 'chill_person_household_relationship',
'routeParameters' => [
'household_id' => $household->getId(),
], ])
->setExtras(['order' => 15]);
}
public static function getMenuIds(): array

View File

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

View File

@@ -0,0 +1,54 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Menu;
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
use Knp\Menu\MenuItem;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
class UserMenuBuilder implements LocalMenuBuilderInterface
{
/**
* @var AuthorizationCheckerInterface
*/
public $authorizationChecker;
/**
* @var TranslatorInterface
*/
public $translator;
public function __construct(
AuthorizationCheckerInterface $authorizationChecker
) {
$this->authorizationChecker = $authorizationChecker;
}
public function buildMenu($menuId, MenuItem $menu, array $parameters)
{
if ($this->authorizationChecker->isGranted('ROLE_USER')) {
$menu->addChild('My accompanying periods', [
'route' => 'chill_person_accompanying_period_user',
])
->setExtras([
'order' => 20,
'icon' => 'tasks',
]);
}
}
public static function getMenuIds(): array
{
return ['user'];
}
}

View File

@@ -0,0 +1,102 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
class AccompanyingPeriodWorkEvaluationRepository implements ObjectRepository
{
private EntityRepository $repository;
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(AccompanyingPeriodWorkEvaluation::class);
}
public function countNearMaxDateByUser(User $user): int
{
return $this->buildQueryNearMaxDateByUser($user)
->select('count(e)')->getQuery()->getSingleScalarResult();
}
public function find($id): ?AccompanyingPeriodWorkEvaluation
{
return $this->repository->find($id);
}
/**
* @return array|AccompanyingPeriodWorkEvaluation[]
*/
public function findAll(): array
{
return $this->repository->findAll();
}
/**
* @param int $limit
* @param int $offset
*
* @return array|AccompanyingPeriodWorkEvaluation[]
*/
public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array
{
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
public function findNearMaxDateByUser(User $user, int $limit = 20, int $offset = 0): array
{
return $this->buildQueryNearMaxDateByUser($user)
->select('e')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation
{
return $this->repository->findOneBy($criteria);
}
public function getClassName(): string
{
return AccompanyingPeriodWorkEvaluation::class;
}
private function buildQueryNearMaxDateByUser(User $user): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('e');
$qb
->join('e.accompanyingPeriodWork', 'work')
->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'))
)
)
->setParameters([
'user' => $user,
'now' => new DateTimeImmutable('now'),
]);
return $qb;
}
}

View File

@@ -11,10 +11,14 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository\AccompanyingPeriod;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Doctrine\Persistence\ObjectRepository;
final class AccompanyingPeriodWorkRepository implements ObjectRepository
@@ -41,6 +45,12 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
->getSingleScalarResult();
}
public function countNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): int
{
return $this->buildQueryNearEndDateByUser($user, $since, $until)
->select('count(w)')->getQuery()->getSingleScalarResult();
}
public function find($id): ?AccompanyingPeriodWork
{
return $this->repository->find($id);
@@ -68,6 +78,16 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return $this->repository->findByAccompanyingPeriod($period, $orderBy, $limit, $offset);
}
public function findNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until, int $limit = 20, int $offset = 0): array
{
return $this->buildQueryNearEndDateByUser($user, $since, $until)
->select('w')
->setFirstResult($offset)
->setMaxResults($limit)
->getQuery()
->getResult();
}
public function findOneBy(array $criteria): ?AccompanyingPeriodWork
{
return $this->repository->findOneBy($criteria);
@@ -78,22 +98,6 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
return AccompanyingPeriodWork::class;
}
public function toDelete()
{
$qb = $this->buildQueryBySocialActionWithDescendants($action);
$qb->select('g');
foreach ($orderBy as $sort => $order) {
$qb->addOrderBy('g.' . $sort, $order);
}
return $qb
->setMaxResults($limit)
->setFirstResult($offset)
->getQuery()
->getResult();
}
private function buildQueryBySocialActionWithDescendants(SocialAction $action): QueryBuilder
{
$actions = $action->getDescendantsWithThis();
@@ -103,12 +107,34 @@ final class AccompanyingPeriodWorkRepository implements ObjectRepository
$orx = $qb->expr()->orX();
$i = 0;
foreach ($actions as $action) {
foreach ($actions as $a) {
$orx->add(":action_{$i} MEMBER OF g.socialActions");
$qb->setParameter("action_{$i}", $action);
$qb->setParameter("action_{$i}", $a);
}
$qb->where($orx);
return $qb;
}
private function buildQueryNearEndDateByUser(User $user, DateTimeImmutable $since, DateTimeImmutable $until): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('w');
$qb
->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')
)
)
->setParameters([
'user' => $user,
'since' => $since,
'until' => $until,
]);
return $qb;
}
}

View File

@@ -11,7 +11,9 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Repository;
use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
@@ -26,6 +28,18 @@ final class AccompanyingPeriodRepository implements ObjectRepository
$this->repository = $entityManager->getRepository(AccompanyingPeriod::class);
}
public function countBy(array $criteria): int
{
return $this->repository->count($criteria);
}
public function countByRecentUserHistory(User $user, DateTimeImmutable $since): int
{
$qb = $this->buildQueryByRecentUserHistory($user, $since);
return $qb->select('count(a)')->getQuery()->getSingleScalarResult();
}
public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder
{
return $this->repository->createQueryBuilder($alias, $indexBy);
@@ -49,6 +63,21 @@ final class AccompanyingPeriodRepository implements ObjectRepository
return $this->repository->findBy($criteria, $orderBy, $limit, $offset);
}
/**
* @return array|AccompanyingPeriod[]
*/
public function findByRecentUserHistory(User $user, DateTimeImmutable $since, ?int $limit = 20, ?int $offset = 0): array
{
$qb = $this->buildQueryByRecentUserHistory($user, $since);
return $qb->select('a')
->distinct(true)
->getQuery()
->setMaxResults($limit)
->setFirstResult($offset)
->getResult();
}
public function findOneBy(array $criteria): ?AccompanyingPeriod
{
return $this->findOneBy($criteria);
@@ -58,4 +87,19 @@ final class AccompanyingPeriodRepository implements ObjectRepository
{
return AccompanyingPeriod::class;
}
private function buildQueryByRecentUserHistory(User $user, DateTimeImmutable $since): QueryBuilder
{
$qb = $this->repository->createQueryBuilder('a');
$qb
->join('a.userHistories', 'userHistory')
->where($qb->expr()->eq('a.user', ':user'))
->andWhere($qb->expr()->gte('userHistory.startDate', ':since'))
->andWhere($qb->expr()->isNull('userHistory.endDate'))
->setParameter('user', $user)
->setParameter('since', $since);
return $qb;
}
}

View File

@@ -39,7 +39,7 @@ final class HouseholdACLAwareRepository implements HouseholdACLAwareRepositoryIn
{
$centers = $this->authorizationHelper->getReachableCenters(
$this->security->getUser(),
HouseholdVoter::SHOW
HouseholdVoter::SEE
);
if ([] === $centers) {

View File

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

View File

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

View File

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

View File

@@ -80,6 +80,8 @@ div.dashboard {
}
}
div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title {
display: flex;
flex-direction: row;
@@ -128,6 +130,8 @@ ul.columns { // XS:1 SM:2 MD:1 LG:2 XL:2 XXL:2
/// dashboard_like_badge in AccompanyingCourse Work list Page
div[class*='accompanying_course_work'] {
div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
@@ -143,6 +147,8 @@ div[class*='accompanying_course_work'] {
/// dashboard_like_badge in Activities on resume page
div[class*='activity-'] {
div.dashboard,
h4.badge-title,
h3.badge-title,
h2.badge-title {
span.title_label {
// Calculate same color then border:groove
@@ -184,5 +190,9 @@ div[class*='activity-'] {
background-color: $chill-ll-gray;
color: $chill-blue;
}
&.bg-confidential {
background-color: $chill-ll-gray;
color: $chill-red;
}
}

View File

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

View File

@@ -0,0 +1,61 @@
function capitalizeFirstLetter(string) {
return string.charAt(0).toLocaleUpperCase() + string.slice(1);
}
window.addEventListener('DOMContentLoaded', function() {
const uri = decodeURI(location.hash.substring(1))
let searchFragments = uri.split(' ')
searchFragments = searchFragments.filter((el) => {
if ( ( el.startsWith("firstname") || el.startsWith("lastname") ) || (el !== '' && !el.startsWith('birthdate') && !el.startsWith('gender') && !el.startsWith('city') && !el.startsWith('phonenumber') && !el.startsWith('@'))) {
return el
}
})
searchFragments = searchFragments.map((el) => {
if (el.startsWith("firstname")) {
return el.slice(10)
} else if (el.startsWith("lastname")) {
return el.slice(10)
}
return el.replace('\"', '')
})
if (searchFragments) {
const pre = '<ul class="list-suggest add-items inline">';
const after = '</ul>';
document.querySelectorAll('[data-suggest-container]').forEach(function(container) {
const suggestions = searchFragments.map((el) => `<li class="suggest-item-name"><span data-suggest-target="${container.dataset.suggestContainer}">${capitalizeFirstLetter(el)}</span></li>`);
container.innerHTML = pre + suggestions.join(' ') + after;
})
}
const tags = document.querySelectorAll('[data-suggest-target]').forEach((tag) => {
tag.addEventListener('click', function(e) {
const field = document.querySelector(`[name="${e.target.dataset.suggestTarget}"]`);
let suggestion = e.target.textContent.trim();
switch (field.dataset.suggestTransform) {
case 'uppercase_all':
suggestion = suggestion.toLocaleUpperCase();
break;
case 'uppercase_first_letter':
default:
suggestion = capitalizeFirstLetter(suggestion);
}
if (field.value === '') {
field.value = suggestion;
} else {
field.value = `${field.value} ${suggestion}`
}
e.target.style.display = "none";
[...document.querySelectorAll("[data-suggest-target]")]
.filter(p => p.textContent.includes(e.target.textContent))
.forEach(p => p.remove());
})
})
})

View File

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

View File

@@ -14,6 +14,7 @@
<scopes></scopes>
<referrer></referrer>
<resources></resources>
<start-date v-if="accompanyingCourse.step === 'CONFIRMED'"></start-date>
<comment v-if="accompanyingCourse.step === 'DRAFT'"></comment>
<confirm v-if="accompanyingCourse.step === 'DRAFT'"></confirm>
@@ -39,6 +40,7 @@ import Referrer from './components/Referrer.vue';
import Resources from './components/Resources.vue';
import Comment from './components/Comment.vue';
import Confirm from './components/Confirm.vue';
import StartDate from './components/StartDate.vue';
export default {
name: 'App',
@@ -56,6 +58,7 @@ export default {
Resources,
Comment,
Confirm,
StartDate
},
computed: {
...mapState([

View File

@@ -15,21 +15,15 @@ const getAccompanyingCourse = (id) => {
});
};
const getUsers = () => {
const url = `/api/1.0/main/user.json`;
return fetchResults(url);
};
const getUsers = () => fetchResults('/api/1.0/main/user.json');
const getReferrersSuggested = (course) => {
const url = `/api/1.0/person/accompanying-course/${course.id}/referrers-suggested.json`;
return fetchResults(url);
}
/*
* Endpoint
*/
const getUserJobs = () => fetchResults('/api/1.0/main/user-job.json');
const getSocialIssues = () => {
const url = `/api/1.0/person/social-work/social-issue.json`;
return fetch(url)
@@ -54,4 +48,5 @@ export {
getAccompanyingCourse,
getUsers,
getReferrersSuggested,
getUserJobs
};

View File

@@ -19,6 +19,9 @@
:options="options"
group-values="locations"
group-label="locationCategories"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
@select="updateAdminLocation">
</VueMultiselect>
</div>

View File

@@ -4,7 +4,7 @@
<i class="fa fa-home fa-fw text-light" :title="$t('persons_associated.show_household_number', { id: h.id })"></i>
</a>
<span v-for="person in h.persons" class="me-1" :key="person.id">
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.text" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
<on-the-fly :type="person.type" :id="person.id" :buttonText="person.textAge" :displayBadge="'true' === 'true'" action="show"></on-the-fly>
</span>
</span>
</template>

View File

@@ -59,9 +59,35 @@
</template>
<template v-slot:body>
<p>{{ $t('confirm.sure_description') }}</p>
<div v-if="accompanyingCourse.user === null">
<div v-if="filteredReferrersSuggested.length === 0">
<p class="alert alert-warning">{{ $t('confirm.no_suggested_referrer') }}</p>
</div>
<div v-if="filteredReferrersSuggested.length === 1" class="alert alert-info">
<p>{{ $t('confirm.one_suggested_referrer') }}:</p>
<ul class="list-suggest add-items inline">
<li>
<user-render-box-badge :user="filteredReferrersSuggested[0]"></user-render-box-badge>
</li>
</ul>
<p>{{ $t('confirm.choose_suggested_referrer') }}</p>
<ul class="record_actions">
<li>
<button class="btn btn-save mr-5" @click="chooseSuggestedReferrer">
{{ $t('confirm.choose_button') }}
</button>
</li>
<li>
<button class="btn btn-secondary" @click="doNotChooseSuggestedReferrer">
{{ $t('confirm.do_not_choose_button') }}
</button>
</li>
</ul>
</div>
</div>
</template>
<template v-slot:footer>
<button class="btn btn-danger" @click="confirmCourse">
<button class="btn btn-danger" :disabled="disableConfirm" @click="confirmCourse">
{{ $t('confirm.ok') }}
</button>
</template>
@@ -74,11 +100,13 @@
<script>
import {mapGetters, mapState} from "vuex";
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge";
export default {
name: "Confirm",
components: {
Modal,
UserRenderBoxBadge
},
data() {
return {
@@ -110,26 +138,38 @@ export default {
scopes: {
msg: 'confirm.set_a_scope',
anchor: '#section-70'
}
}
},
job: {
msg: 'confirm.job_not_valid',
anchor: '#section-80'
},
},
clickedDoNotChooseReferrer: false
}
},
computed: {
...mapState([
'accompanyingCourse'
]),
...mapState({
accompanyingCourse: state => state.accompanyingCourse,
filteredReferrersSuggested: state => state.filteredReferrersSuggested
}),
...mapGetters([
'isParticipationValid',
'isSocialIssueValid',
'isOriginValid',
'isAdminLocationValid',
'isLocationValid',
'isJobValid',
'validationKeys',
'isValidToBeConfirmed'
]),
deleteLink() {
return `/fr/parcours/${this.accompanyingCourse.id}/delete`; //TODO locale
},
disableConfirm() {
return this.clickedDoNotChooseReferrer
? (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0)
: (this.accompanyingCourse.user === null && this.filteredReferrersSuggested.length === 0) || (this.filteredReferrersSuggested.length === 1);
}
},
methods: {
confirmCourse() {
@@ -141,6 +181,19 @@ export default {
this.$toast.open({message: 'An error occurred'})
}
});
},
chooseSuggestedReferrer() {
this.$store.dispatch('updateReferrer', this.filteredReferrersSuggested[0])
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
doNotChooseSuggestedReferrer() {
this.clickedDoNotChooseReferrer = true;
}
}
}

View File

@@ -27,7 +27,7 @@
:value="p.person.id"
/>
<label class="form-check-label">
{{ p.person.text }}
<person-text :person="p.person"></person-text>
</label>
</div>
<input type="hidden" name="expand_suggestions" value="true">
@@ -50,9 +50,9 @@
<div v-if="suggestedPersons.length > 0">
<ul class="list-suggest add-items inline">
<li v-for="p in suggestedPersons" :key="p.id" @click="addSuggestedPerson(p)">
<span>{{ p.text }}</span>
</li>
</ul>
<person-text :person="p"></person-text>
</li>
</ul>
</div>
<div>
@@ -76,12 +76,14 @@
import {mapGetters, mapState} from 'vuex';
import ParticipationItem from "./PersonsAssociated/ParticipationItem.vue";
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: 'PersonsAssociated',
components: {
ParticipationItem,
AddPersons
AddPersons,
PersonText
},
data() {
return {
@@ -110,15 +112,15 @@ export default {
)
// filter persons appearing twice in requestor and resources
.filter(
(e, index, suggested) => {
(e, index, suggested) => {
for (let i = 0; i < suggested.length; i = i+1) {
if (i < index && e.id === suggested[i].id) {
return false
}
if (i < index && e.id === suggested[i].id) {
return false
}
}
return true;
}
}
)
}),
...mapGetters([

View File

@@ -28,7 +28,7 @@
</a>
</li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<li><on-the-fly :type="participation.person.type" :id="participation.person.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" :canCloseModal="canCloseOnTheFlyModal"></on-the-fly></li>
<li>
<button v-if="!participation.endDate"
class="btn btn-sm btn-remove"
@@ -63,6 +63,7 @@ import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default {
name: 'ParticipationItem',
@@ -88,7 +89,8 @@ export default {
addAge: false,
hLevel: 1
}
}
},
canCloseOnTheFlyModal: false
}
},
computed: {
@@ -110,14 +112,53 @@ export default {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'participation';
this.$store.dispatch('patchOnTheFly', 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'})
}
});
let body = { type: payload.type };
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
}
}
}

View File

@@ -3,6 +3,27 @@
<h2><a id="section-80"></a>{{ $t('referrer.title') }}</h2>
<div>
<label class="col-form-label" for="selectJob">
{{ $t('job.label') }}
</label>
<VueMultiselect
name="selectJob"
label="text"
:custom-label="customJobLabel"
track-by="id"
:multiple="false"
:searchable="true"
:placeholder="$t('job.placeholder')"
v-model="valueJob"
:options="jobs"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
@select="updateJob">
</VueMultiselect>
<label class="col-form-label" for="selectReferrer">
{{ $t('referrer.label') }}
</label>
@@ -15,16 +36,16 @@
:searchable="true"
:placeholder="$t('referrer.placeholder')"
v-model="value"
v-bind:options="users"
:options="users"
:select-label="$t('multiselect.select_label')"
:deselect-label="$t('multiselect.deselect_label')"
:selected-label="$t('multiselect.selected_label')"
@select="updateReferrer">
</VueMultiselect>
<template v-if="referrersSuggested.length > 0">
<template v-if="filteredReferrersSuggested.length > 0">
<ul class="list-suggest add-items inline">
<li v-for="(u, i) in referrersSuggested" @click="updateReferrer(u)" :key="`referrer-${i}`">
<li v-for="(u, i) in filteredReferrersSuggested" @click="updateReferrer(u)" :key="`referrer-${i}`">
<span>
<user-render-box-badge :user="u"></user-render-box-badge>
</span>
@@ -47,13 +68,17 @@
</ul>
</div>
<div v-if="!isJobValid" class="alert alert-warning to-confirm">
{{ $t('job.not_valid') }}
</div>
</div>
</template>
<script>
import VueMultiselect from 'vue-multiselect';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge";
export default {
@@ -62,23 +87,33 @@ export default {
UserRenderBoxBadge,
VueMultiselect,
},
data() {
return {
jobs: []
}
},
computed: {
...mapState({
value: state => state.accompanyingCourse.user,
users: state => state.users,
referrersSuggested: state => {
return state.referrersSuggested.filter(u => {
if (null === state.accompanyingCourse.user) {
return true;
}
return state.accompanyingCourse.user.id !== u.id;
})
},
valueJob: state => state.accompanyingCourse.job,
users: state => state.users.filter(u => {
if (u.user_job && state.accompanyingCourse.job) {
return u.user_job.id === state.accompanyingCourse.job.id;
} else {
return false;
}
}),
filteredReferrersSuggested: state => state.filteredReferrersSuggested,
}),
...mapGetters([
'isJobValid'
])
},
mounted() {
this.getJobs();
},
methods: {
updateReferrer(value) {
//console.log('value', value);
this.$store.dispatch('updateReferrer', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
@@ -88,6 +123,29 @@ export default {
}
});
},
getJobs() {
const url = '/api/1.0/main/user-job.json';
makeFetch('GET', url)
.then(response => {
this.jobs = response.results;
})
.catch((error) => {
this.$toast.open({message: error.txt})
})
},
updateJob(value) {
this.$store.dispatch('updateJob', value)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
customJobLabel(value) {
return value.label.fr;
},
assignMe() {
const url = `/api/1.0/main/whoami.json`;
makeFetch('GET', url)

View File

@@ -45,7 +45,8 @@
addInfo: true,
hLevel: 3,
isMultiline: true,
isConfidential: false
isConfidential: false,
addAge: true,
}"
>
<template v-slot:record-actions>
@@ -113,7 +114,7 @@
<template v-slot:record-actions>
<ul class="record_actions">
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="show"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly"></on-the-fly></li>
<li><on-the-fly :type="accompanyingCourse.requestor.type" :id="accompanyingCourse.requestor.id" action="edit" @saveFormOnTheFly="saveFormOnTheFly" :canCloseModal="canCloseOnTheFlyModal"></on-the-fly></li>
</ul>
</template>
</person-render-box>
@@ -136,9 +137,10 @@
<div v-if="accompanyingCourse.requestor === null && suggestedEntities.length > 0">
<ul class="list-suggest add-items inline">
<li v-for="p in suggestedEntities" :key="uniqueId(p)" @click="addSuggestedEntity(p)">
<span>{{ p.text }}</span>
</li>
</ul>
<person-text v-if="p.type === 'person'" :person="p"></person-text>
<span v-else>{{ p.text }}</span>
</li>
</ul>
</div>
<div>
@@ -162,6 +164,9 @@ import PersonRenderBox from '../../_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
import { mapState } from 'vuex';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: 'Requestor',
@@ -170,7 +175,8 @@ export default {
OnTheFly,
PersonRenderBox,
ThirdPartyRenderBox,
Confidential
Confidential,
PersonText
},
props: ['isAnonymous'],
data() {
@@ -182,7 +188,8 @@ export default {
priority: null,
uniq: true,
}
}
},
canCloseOnTheFlyModal: false
}
},
computed: {
@@ -246,14 +253,52 @@ export default {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'requestor';
this.$store.dispatch('patchOnTheFly', 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'})
}
});
let body = { type: payload.type };
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
},
addSuggestedEntity(e) {
this.$store.dispatch('addRequestor', { result: e, type: e.type })

View File

@@ -22,7 +22,8 @@
<div v-if="suggestedEntities.length > 0">
<ul class="list-suggest add-items inline">
<li v-for="p in suggestedEntities" :key="uniqueId(p)" @click="addSuggestedEntity(p)">
<span>{{ p.text }}</span>
<person-text v-if="p.type === 'person'" :person="p"></person-text>
<span v-else>{{ p.text }}</span>
</li>
</ul>
</div>
@@ -45,12 +46,15 @@
import { mapState } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import ResourceItem from './Resources/ResourceItem.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: 'Resources',
components: {
AddPersons,
ResourceItem
ResourceItem,
PersonText
},
data() {
return {

View File

@@ -34,7 +34,8 @@
:type="resource.resource.type"
:id="resource.resource.id"
action="edit"
@saveFormOnTheFly="saveFormOnTheFly">
@saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal">
</on-the-fly>
</li>
<li>
@@ -80,7 +81,8 @@
:type="resource.resource.type"
:id="resource.resource.id"
action="edit"
@saveFormOnTheFly="saveFormOnTheFly">
@saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal">
</on-the-fly>
</li>
<li>
@@ -101,6 +103,7 @@ import ButtonLocation from '../ButtonLocation.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import WriteComment from './WriteComment';
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default {
name: 'ResourceItem',
@@ -113,6 +116,11 @@ export default {
},
props: ['resource'],
emits: ['remove'],
data() {
return {
canCloseOnTheFlyModal: false
}
},
computed: {
parent() {
return {
@@ -136,14 +144,52 @@ export default {
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource';
this.$store.dispatch('patchOnTheFly', 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'})
}
});
let body = { type: payload.type };
if (payload.type === 'person') {
body.firstName = payload.data.firstName;
body.lastName = payload.data.lastName;
if (payload.data.birthdate !== null) { body.birthdate = payload.data.birthdate; }
body.phonenumber = payload.data.phonenumber;
body.mobilenumber = payload.data.mobilenumber;
body.gender = payload.data.gender;
makeFetch('PATCH', `/api/1.0/person/person/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addPerson', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
else if (payload.type === 'thirdparty') {
body.name = payload.data.text;
body.email = payload.data.email;
body.telephone = payload.data.phonenumber;
body.address = { id: payload.data.address.address_id };
makeFetch('PATCH', `/api/1.0/third-party/third-party/${payload.data.id}.json`, body)
.then(response => {
this.$store.dispatch('addThirdparty', { target: payload.target, body: response })
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
},
updateComment(resource) {
console.log('updateComment', resource);

View File

@@ -7,7 +7,7 @@
-->
<VueMultiselect
name="field"
:close-on-select="false"
:close-on-select="true"
:allow-empty="true"
:show-labels="false"
track-by="id"

View File

@@ -0,0 +1,46 @@
<template>
<div class="vue-component">
<h2><a id="section-110"></a>
{{ $t('startdate.change') }}
</h2>
<div>
<div class="mb-3 row">
<label class="col-form-label col-sm-4">{{ $t('startdate.date') }}</label>
<div class="col-sm-8">
<input class="form-control" type="date" v-model="startDate" @change="updateStartDate" />
</div>
</div>
</div>
</div>
</template>
<script>
import { datetimeToISO, dateToISO, ISOToDate, ISOToDatetime} from 'ChillMainAssets/chill/js/date.js';
import { mapState, mapGetters } from 'vuex';
export default {
name: 'startDate',
methods: {
updateStartDate(event) {
const date = event.target.value;
// console.log(date)
this.$store.dispatch('updateStartDate', date)
.catch(({name, violations}) => {
if (name === 'ValidationException' || name === 'AccessException') {
violations.forEach((violation) => this.$toast.open({message: violation}));
} else {
this.$toast.open({message: 'An error occurred'})
}
});
},
},
computed: {
...mapState({
startDate: state => dateToISO(ISOToDatetime(state.accompanyingCourse.openingDate.datetime))
})
}
}
</script>

View File

@@ -13,7 +13,7 @@ const root = window.vueRootComponent;
* Load all App component, for AccompanyingCourse edition page
*/
if (root === 'app') {
initPromise.then(store => {
initPromise(root).then(store => {
const i18n = _createI18n(appMessages);
@@ -37,7 +37,7 @@ if (root === 'app') {
* Load only Banner sub-component, for all others AccompanyingCourse page
*/
if (root === 'banner') {
initPromise.then(store => {
initPromise(root).then(store => {
const i18n = _createI18n(appMessages);

View File

@@ -102,7 +102,7 @@ const appMessages = {
no_address: "Il n'y a pas d'adresse associée au parcours"
},
scopes: {
title: "Services",
title: "Services concernés",
add_at_least_one: "Indiquez au moins un service",
},
referrer: {
@@ -134,11 +134,26 @@ const appMessages = {
location_not_valid: "indiquez au minimum une localisation temporaire du parcours",
origin_not_valid: "Indiquez une origine à la demande",
adminLocation_not_valid: "Indiquez une localisation administrative à la demande",
job_not_valid: "Indiquez un métier du référent",
set_a_scope: "indiquez au moins un service",
sure: "Êtes-vous sûr ?",
sure_description: "Une fois le changement confirmé, il ne sera plus possible de le remettre à l'état de brouillon !",
ok: "Confirmer le parcours",
delete: "Supprimer le parcours"
delete: "Supprimer le parcours",
no_suggested_referrer: "Il n'y a aucun référent qui puisse être désigné pour ce parcours. Vérifiez la localisation du parcours, les métiers et service indiqués. Si le problème persiste, contactez l'administrateur du logiciel.",
one_suggested_referrer: "Un unique référent peut être suggéré pour ce parcours",
choose_suggested_referrer: "Voulez-vous le désigner directement ?",
choose_button: "Désigner",
do_not_choose_button: "Ne pas désigner"
},
job: {
label: "Métier",
placeholder: "Choisir un métier",
not_valid: "Sélectionnez un métier du référent"
},
startdate: {
change: "Date d'ouverture",
date: "Date d'ouverture",
},
// catch errors
'Error while updating AccompanyingPeriod Course.': "Erreur du serveur lors de la mise à jour du parcours d'accompagnement.",

View File

@@ -8,15 +8,20 @@ import { getAccompanyingCourse,
import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
import { datetimeToISO, ISOToDate, ISOToDatetime } from 'ChillMainAssets/chill/js/date.js';
const debug = process.env.NODE_ENV !== 'production';
const id = window.accompanyingCourseId;
let scopesPromise = fetchScopes();
let getScopesPromise = (root) => {
if (root === 'app') {
return fetchScopes();
}
}
let accompanyingCoursePromise = getAccompanyingCourse(id);
let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
let initPromise = (root) => Promise.all([getScopesPromise(root), accompanyingCoursePromise])
.then(([scopes, accompanyingCourse]) => new Promise((resolve, reject) => {
const store = createStore({
@@ -35,6 +40,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
scopesAtBackend: accompanyingCourse.scopes.map(scope => scope),
// the users which are available for referrer
referrersSuggested: [],
filteredReferrersSuggested: [],
// all the users available
users: [],
permissions: {}
@@ -55,6 +61,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
isLocationValid(state) {
return state.accompanyingCourse.location !== null;
},
isJobValid(state) {
return state.accompanyingCourse.job !== null;
},
isScopeValid(state) {
//console.log('is scope valid', state.accompanyingCourse.scopes.length > 0);
return state.accompanyingCourse.scopes.length > 0;
@@ -63,6 +72,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
let keys = [];
if (!getters.isParticipationValid) { keys.push('participation'); }
if (!getters.isLocationValid) { keys.push('location'); }
if (!getters.isJobValid) { keys.push('job'); }
if (!getters.isSocialIssueValid) { keys.push('socialIssue'); }
if (!getters.isOriginValid) { keys.push('origin'); }
if (!getters.isAdminLocationValid) { keys.push('adminLocation'); }
@@ -193,6 +203,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
//console.log('value', value);
state.accompanyingCourse.user = value;
},
updateJob(state, value) {
state.accompanyingCourse.job = value;
},
setReferrersSuggested(state, users) {
state.referrersSuggested = users.map(u => {
if (state.accompanyingCourse.user !== null) {
@@ -203,6 +216,22 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
return u;
});
},
setFilteredReferrersSuggested(state) {
state.filteredReferrersSuggested = state.referrersSuggested.filter(u => {
if (u.user_job && state.accompanyingCourse.job && state.accompanyingCourse.user) {
return u.user_job.id === state.accompanyingCourse.job.id && state.accompanyingCourse.user.id !== u.id
} else {
if (null === state.accompanyingCourse.user) {
if (u.user_job && state.accompanyingCourse.job) {
return u.user_job.id === state.accompanyingCourse.job.id
} else {
return true;
}
}
return state.accompanyingCourse.user.id !== u.id;
}
})
},
confirmAccompanyingCourse(state, response) {
//console.log('### mutation: confirmAccompanyingCourse: response', response);
state.accompanyingCourse.step = response.step;
@@ -254,6 +283,10 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
if (scopeIds.includes(scope.id)) {
state.scopesAtBackend = state.scopesAtBackend.filter(s => s.id !== scope.id);
}
},
updateStartDate(state, date) {
console.log('new state date', date)
state.accompanyingCourse.openingDate = date;
}
},
actions: {
@@ -262,6 +295,12 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
commit('removeParticipation', payload);
// fetch DELETE request...
},
addPerson({ commit }, payload) {
commit('updatePerson', { target: payload.target, person: payload.body });
},
addThirdparty({ commit }, payload) {
commit('updateThirdparty', { target: payload.target, thirdparty: payload.body });
},
/**
* Add/close participation
*/
@@ -624,8 +663,8 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
})
},
updateOrigin({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const body = { type: "accompanying_period", origin: { id: payload.id, type: payload.type }}
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", origin: { id: payload.id, type: payload.type }};
return makeFetch('PATCH', url, body)
.then((response) => {
@@ -637,8 +676,8 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
})
},
updateAdminLocation({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const body = { type: "accompanying_period", administrativeLocation: { id: payload.id, type: payload.type }}
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", administrativeLocation: { id: payload.id, type: payload.type }};
return makeFetch('PATCH', url, body)
.then((response) => {
@@ -650,12 +689,42 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
})
},
updateReferrer({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`
const body = { type: "accompanying_period", user: { id: payload.id, type: payload.type }}
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", user: { id: payload.id, type: payload.type }};
return makeFetch('PATCH', url, body)
.then((response) => {
commit('updateReferrer', response.user);
commit('setFilteredReferrersSuggested');
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
updateJob({ commit }, payload) {
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", job: { id: payload.id, type: payload.type }};
return makeFetch('PATCH', url, body)
.then((response) => {
commit('updateJob', response.job);
commit('setFilteredReferrersSuggested');
})
.catch((error) => {
commit('catchError', error);
throw error;
})
},
updateStartDate({commit}, payload) {
console.log('payload', payload)
const date = ISOToDate(payload);
const url = `/api/1.0/person/accompanying-course/${id}.json`;
const body = { type: "accompanying_period", openingDate: { datetime: datetimeToISO(date) }};
console.log('body', body)
return makeFetch('PATCH', url, body)
.then((response) => {
commit('updateStartDate', response.openingDate);
})
.catch((error) => {
commit('catchError', error);
@@ -665,11 +734,12 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
async fetchReferrersSuggested({ state, commit}) {
let users = await getReferrersSuggested(state.accompanyingCourse);
commit('setReferrersSuggested', users);
commit('setFilteredReferrersSuggested');
if (
null === state.accompanyingCourse.user
&& !state.accompanyingCourse.confidential
&& !state.accompanyingCourse.step === 'DRAFT'
&& users.length === 1
null === state.accompanyingCourse.user
&& !state.accompanyingCourse.confidential
&& !state.accompanyingCourse.step === 'DRAFT'
&& users.length === 1
) {
// set the user if unique
commit('updateReferrer', users[0]);
@@ -750,8 +820,11 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise])
}
});
store.dispatch('fetchReferrersSuggested');
store.dispatch('fetchUsers');
if (root === 'app') {
store.dispatch('fetchReferrersSuggested');
store.dispatch('fetchUsers');
}
resolve(store);
}));

View File

@@ -5,7 +5,7 @@
<div id="awc_create_form">
<div id="picking">
<div id="picking" class="">
<p>{{ $t('pick_social_issue_linked_with_action') }}</p>
<div v-for="si in socialIssues" :key="si.id">
<input type="radio" v-bind:value="si.id" name="socialIssue" v-model="socialIssuePicked"><span class="badge bg-chill-l-gray text-dark">{{ si.text }}</span>
@@ -33,7 +33,7 @@
</vue-multiselect>
</div>
</div>
<div v-if="hasSocialIssuePicked">
<div v-if="hasSocialIssuePicked" class="mb-3">
<h2>{{ $t('pick_an_action') }}</h2>
<div class="col-11">
<vue-multiselect
@@ -52,26 +52,43 @@
<i class="fa fa-circle-o-notch fa-spin fa-fw"></i>
</div>
<div v-if="hasSocialActionPicked" id="persons">
<div v-if="hasSocialActionPicked" id="persons" class="mb-5">
<h2>{{ $t('persons_involved') }}</h2>
<ul>
<li v-for="p in personsReachables" :key="p.id">
<input type="checkbox" :value="p.id" v-model="personsPicked">
<person-render-box render="badge" :options="{}" :person="p"></person-render-box>
<div class="form-check">
<input type="checkbox" :value="p.id" v-model="personsPicked" class="form-check-input" :id="'person_check'+p.id">
<label class="form-check-label" :for="'person_check' + p.id">
<person-text :person="p"></person-text>
</label>
</div>
</li>
</ul>
</div>
</div>
<div v-if="hasSocialActionPicked" id="start_date">
<!-- <div v-if="hasSocialActionPicked" id="start_date">
<p><label>{{ $t('startDate') }}</label> <input type="date" v-model="startDate" /></p>
</div> -->
<div class="row">
<div v-if="hasSocialActionPicked" id="start_date" class="mb-3 row">
<label class="col-form-label col-sm-4">{{ $t('startDate') }}</label>
<div class="col-sm-8">
<input class="form-control" type="date" v-model="startDate"/>
</div>
</div>
<div v-if="hasSocialActionPicked" id="end_date">
<!-- <div v-if="hasSocialActionPicked" id="end_date">
<p><label>{{ $t('endDate') }}</label> <input type="date" v-model="endDate" /></p>
</div> -->
<div v-if="hasSocialActionPicked" id="end_date" class="mb-3 row">
<label class="col-form-label col-sm-4">{{ $t('endDate') }}</label>
<div class="col-sm-8">
<input class="form-control" type="date" v-model="endDate"/>
</div>
</div>
</div>
<div id="confirm">
<div v-if="hasErrors">
<p>{{ $t('form_has_errors') }}</p>
@@ -111,7 +128,7 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import VueMultiselect from 'vue-multiselect';
import { dateToISO, ISOToDate } from 'ChillMainAssets/chill/js/date.js';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
const i18n = {
messages: {
@@ -133,7 +150,7 @@ export default {
name: 'App',
components: {
VueMultiselect,
PersonRenderBox,
PersonText,
},
methods: {
submit() {

View File

@@ -141,10 +141,12 @@
<ul class="list-unstyled">
<li v-for="p in personsReachables" :key="p.id">
<label :for="p.id">
<input v-model="personsPicked" :value="p.id" :id="p.id" type="checkbox" class="me-2">
{{ p.text }}
<div class="form-check">
<input v-model="personsPicked" :value="p.id" type="checkbox" class="me-2 form-check-input" :id="'person_check'+p.id">
<label :for="'person_check'+p.id" class="form-check-label">
<person-text :person="p"></person-text>
</label>
</div>
</li>
</ul>
</div>
@@ -233,18 +235,6 @@
</ul>
</div>
<div>
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork"
:templates="this.templatesAvailablesForAction"
:entityId="work.id"
:beforeMove="beforeGenerateTemplate">
<template v-slot:title>
<h3>{{ $t('Generate doc') }}</h3>
</template>
</pick-template>
</div>
<div v-if="errors.length > 0" id="errors" class="alert alert-danger flashbag">
<p>{{ $t('fix_these_errors') }}</p>
<ul>
@@ -253,7 +243,17 @@
</div>
</div>
<ul class="record_actions sticky-form-buttons">
<ul class="record_actions sticky-form-buttons">
<li>
<list-workflow-modal
:workflows="this.work.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork"
:relatedEntityId="this.work.id"
:workflowsAvailables="this.work.workflows_availables"
@go-to-generate-workflow="goToGenerateWorkflow"
></list-workflow-modal>
</li>
<li v-if="!isPosting">
<button class="btn btn-save" @click="submit">
@@ -282,6 +282,10 @@ import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRe
import ThirdPartyRenderBox from 'ChillThirdPartyAssets/vuejs/_components/Entity/ThirdPartyRenderBox.vue';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import PickWorkflow from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
const i18n = {
messages: {
@@ -329,7 +333,10 @@ export default {
AddressRenderBox,
ThirdPartyRenderBox,
PickTemplate,
OnTheFly
ListWorkflowModal,
OnTheFly,
PickWorkflow,
PersonText,
},
i18n,
data() {
@@ -359,11 +366,11 @@ export default {
display: false
}
},
}
},
};
},
computed: {
...mapState([
computed: {
...mapState([
'work',
'resultsForAction',
'evaluationsForAction',
@@ -455,13 +462,18 @@ export default {
removeThirdParty(t) {
this.$store.commit('removeThirdParty', t);
},
goToGenerateWorkflow({link}) {
console.log('save before leave to generate workflow')
const callback = (data) => {
window.location.assign(link);
};
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
},
submit() {
this.$store.dispatch('submit');
},
beforeGenerateTemplate() {
console.log('before generate');
return Promise.resolve();
},
saveFormOnTheFly(payload) {
console.log('saveFormOnTheFly: type', payload.type, ', data', payload.data);
payload.target = 'resource';

View File

@@ -1,12 +1,26 @@
<template>
<div>
<div class="item-title">
<a id="evaluations"></a>
<div class="item-title" :title="evaluation.id || 'no id yet'">
<span>{{ evaluation.evaluation.title.fr }}</span>
</div>
<div>
<form-evaluation ref="FormEvaluation" :key="evaluation.key" :evaluation="evaluation"></form-evaluation>
<ul class="record_actions">
<ul class="record_actions">
<li v-if="evaluation.workflows_availables.length > 0">
<list-workflow-modal
:workflows="evaluation.workflows"
:allowCreate="true"
relatedEntityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:relatedEntityId="evaluation.id"
:workflowsAvailables="evaluation.workflows_availables"
@go-to-generate-workflow="goToGenerateWorkflow"
></list-workflow-modal>
</li>
<li>
<a class="btn btn-delete" @click="modal.showModal = true" :title="$t('action.delete')"></a>
</li>
@@ -34,6 +48,8 @@
<script>
import FormEvaluation from './FormEvaluation.vue';
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import ListWorkflowModal from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflowModal.vue';
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
const i18n = {
messages: {
@@ -60,7 +76,8 @@ export default {
name: "AddEvaluation",
components: {
FormEvaluation,
Modal
Modal,
ListWorkflowModal,
},
props: ['evaluation'],
i18n,
@@ -88,10 +105,18 @@ export default {
submitForm() {
this.toggleEditEvaluation();
},
buildEditLink(storedObject) {
return `/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash);
},
goToGenerateWorkflow({event, link, workflowName}) {
console.log('goToGenerate in evaluation', event, link, workflowName);
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
window.location.assign(buildLinkCreate(workflowName,
'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation', evaluationId));
};
return this.$store.dispatch('submit', callback)
.catch(e => { console.log(e); throw e; });
}
}
}
</script>

View File

@@ -61,12 +61,39 @@
</div>
</div>
<div v-if="evaluation.documents.length > 0" class="row mb-3">
<h5>{{ $t('Documents') }} :</h5>
<div class="flex-table">
<div class="item-bloc" v-for="d in evaluation.documents">
<div class="item-row">
<div class="item-col"><h6>{{ d.template.name.fr }}</h6></div>
<div class="item-col">
<p>Créé par {{ d.createdBy.text }}<br/>
Le {{ $d(ISOToDatetime(d.createdAt.datetime), 'long') }}</p>
</div>
</div>
<div class="item-row">
<ul class="record_actions" >
<li>
<a :href="buildEditLink(d.storedObject)" class="btn btn-action btn-sm">
<i class="fa fa-edit"></i>
</a>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<pick-template
entityClass="Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation"
:id="evaluation.id"
:templates="getTemplatesAvailables"
:beforeMove="submitBeforeGenerate"
:preventDefaultMoveToGenerate="true"
@go-to-generate-document="submitBeforeGenerate"
>
<template v-slot:title>
<label class="col-sm-4 col-form-label">{{ $t('evaluation_generate_a_document') }}</label>
@@ -83,6 +110,7 @@ import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from 'ChillMainAssets/module/ckeditor5/index.js';
import { mapGetters, mapState } from 'vuex';
import PickTemplate from 'ChillDocGeneratorAssets/vuejs/_components/PickTemplate.vue';
import {buildLink} from 'ChillDocGeneratorAssets/lib/document-generator';
const i18n = {
messages: {
@@ -97,9 +125,10 @@ const i18n = {
evaluation_public_comment: "Note publique",
evaluation_comment_placeholder: "Commencez à écrire ...",
evaluation_generate_a_document: "Générer un document",
evaluation_choose_a_template: "Choisir un gabarit",
evaluation_choose_a_template: "Choisir un modèle",
evaluation_add_a_document: "Ajouter un document",
evaluation_add: "Ajouter une évaluation"
evaluation_add: "Ajouter une évaluation",
Documents: "Documents",
}
}
};
@@ -163,6 +192,7 @@ export default {
},
},
methods: {
ISOToDatetime,
listAllStatus() {
console.log('load all status');
let url = `/api/`;
@@ -175,10 +205,15 @@ export default {
})
;
},
submitBeforeGenerate() {
buildEditLink(storedObject) {
return `/wopi/edit/${storedObject.uuid}?returnPath=` + encodeURIComponent(
window.location.pathname + window.location.search + window.location.hash);
},
submitBeforeGenerate({template}) {
const callback = (data) => {
let evaluationId = data.accompanyingPeriodWorkEvaluations.find(e => e.key === this.evaluation.key).id;
return Promise.resolve({entityId: evaluationId});
window.location.assign(buildLink(template, evaluationId, 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWorkEvaluation'));
};
return this.$store.dispatch('submit', callback).catch(e => { console.log(e); throw e; });

View File

@@ -58,7 +58,11 @@ const store = createStore({
return state.thirdParties.length > 0;
},
getTemplatesAvailablesForEvaluation: (state) => (evaluation) => {
return state.templatesAvailablesForEvaluation.get(evaluation.id) || [];
if (state.templatesAvailablesForEvaluation.has(evaluation.id)) {
return state.templatesAvailablesForEvaluation.get(evaluation.id);
}
return [];
},
buildPayload(state) {
return {
@@ -171,7 +175,6 @@ const store = createStore({
};
g.id = tmpIndex() -1
state.goalsPicked.push(g);
//console.log('goals picked ids', state.goalsPicked.map(g => g.id))
},
removeGoal(state, goal) {
state.goalsPicked = state.goalsPicked.filter(g => g.id !== goal.id);
@@ -205,6 +208,9 @@ const store = createStore({
warningInterval: null,
comment: "",
editEvaluation: true,
workflows_availables: state.work.workflows_availables_evaluation,
documents: [],
workflows: [],
};
state.evaluationsPicked.push(e);
},
@@ -367,11 +373,11 @@ const store = createStore({
return makeFetch('PUT', url, payload)
.then(data => {
console.log('data received', data);
if (typeof(callback) !== 'undefined') {
return callback(data);
} else {
console.info('nothing to do here, bye bye');window.location.assign(`/fr/person/accompanying-period/${state.work.accompanyingPeriod.id}/work`);
console.info('nothing to do here, bye bye');
window.location.assign(`/fr/person/accompanying-period/${state.work.accompanyingPeriod.id}/work`);
}
}).catch(error => {
console.log('error on submit', error);

View File

@@ -32,7 +32,7 @@
</button>
</li>
<li v-else>
<button class="btn btn-save" @click="confirm" :disabled="hasWarnings">
<button class="btn btn-save" @click="confirm" :disabled="hasWarnings || !lastStepIsSaveAllowed">
{{ $t('household_members_editor.app.save') }}
</button>
</li>
@@ -104,6 +104,13 @@ export default {
return false;
},
lastStepIsSaveAllowed() {
let r = !this.$store.getters.isHouseholdNew ||
(this.$store.state.numberOfChildren !== null && this.$store.state.householdCompositionType !== null);
console.log('is saved allowed ?', r);
return r;
},
},
methods: {
goToNext() {

View File

@@ -7,18 +7,14 @@
</div>
</div>
<div v-else>
<p>
{{ $t('household_members_editor.concerned.persons_will_be_moved') }}&nbsp;:
<span v-for="c in concerned" :key="c.person.id">
<person-render-box render="badge" :options="{addLink: false}" :person="c.person"></person-render-box>
<button class="btn" @click="removePerson(c.person)" v-if="c.allowRemove" style="padding-left:0;">
<span class="fa-stack fa-lg" :title="$t('household_members_editor.concerned.remove_concerned')">
<i class="fa fa-circle fa-stack-1x text-danger"></i>
<i class="fa fa-times fa-stack-1x"></i>
</span>
</button>
</span>
</p>
<p>{{ $t('household_members_editor.concerned.persons_will_be_moved') }}&nbsp;:</p>
<ul class="list-suggest remove-items inline">
<li v-for="c in concerned" :key="c.person.id" @click="removeConcerned(c)">
<span><person-text :person="c.person"></person-text></span>
</li>
</ul>
<div class="alert alert-info" v-if="concernedPersonsWithHouseholds.length > 0">
<p>{{ $t('household_members_editor.concerned.persons_with_household') }}</p>
<ul v-for="c in concernedPersonsWithHouseholds" :key="c.person.id">
@@ -61,12 +57,14 @@
import { mapState, mapGetters } from 'vuex';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons.vue';
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: 'Concerned',
components: {
AddPersons,
PersonRenderBox,
PersonText,
},
computed: {
...mapState([
@@ -108,9 +106,14 @@ export default {
this.$refs.addPersons.resetSearch(); // to cast child method
modal.showModal = false;
},
removePerson(person) {
console.log('remove person in concerned', person);
this.$store.dispatch('removePerson', person);
removeConcerned(concerned) {
console.log('removedconcerned', concerned);
if (!concerned.allowRemove) {
return;
}
this.$store.dispatch('removePerson', concerned.person);
},
makeHouseholdLink(id) {
return `/fr/person/household/${id}/summary`

View File

@@ -4,17 +4,38 @@
<h2>{{ $t('household_members_editor.dates.dates_title') }}</h2>
<p>
<label for="start_date">
<div class="mb-3 row">
<label for="start_date" class="col-form-label col-sm-4 required">
{{ $t('household_members_editor.dates.start_date') }}
</label>
<input type="date" v-model="startDate" />
</p>
<div class="col-sm-8">
<input type="date" v-model="startDate" class="form-control" />
</div>
</div>
<div v-if="this.isHouseholdNew">
<h2>{{ $t('household_members_editor.composition.composition') }}</h2>
<div class="mb-3 row">
<label class="col-form-label col-sm-4 required">{{ $t('household_members_editor.composition.household_composition') }}</label>
<div class="col-sm-8">
<select v-model="householdCompositionType" class="form-select form-control">
<option v-for="t in householdCompositionTypes" :key="t.id" :value="t.id">{{ t.label.fr }}</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-form-label col-sm-4 required">{{ $t('household_members_editor.composition.number_of_children') }}</label>
<div class="col-sm-8">
<input type="number" v-model="numberOfChildren" min="0" max="30" class="form-control"/>
</div>
</div>
</div>
</template>
<script>
import CurrentHousehold from "./CurrentHousehold";
import {mapGetters, mapState} from 'vuex';
export default {
name: 'Dates',
@@ -22,6 +43,27 @@ export default {
CurrentHousehold
},
computed: {
...mapState(['householdCompositionTypes']),
...mapGetters(['isHouseholdNew']),
householdCompositionType: {
get() {
if (this.$store.state.householdCompositionType !== null) {
return this.$store.state.householdCompositionType.id;
}
return null;
},
set(value) {
this.$store.commit('setHouseholdCompositionType', value);
},
},
numberOfChildren: {
get() {
return this.$store.state.numberOfChildren;
},
set(value) {
this.$store.commit('setNumberOfChildren', value);
}
},
startDate: {
get() {
return [

View File

@@ -61,6 +61,16 @@
<li v-if="hasHousehold">
<button @click="resetMode" class="btn btn-sm btn-misc">{{ $t('household_members_editor.household.reset_mode')}}</button>
</li>
<li v-if="!hasHousehold">
<add-persons
modalTitle="Chercher un ménage existant"
buttonTitle="Chercher un ménage existant"
v-bind:key="addPersons.key"
v-bind:options="addPersons.options"
@addNewPersons="pickHouseholdFound"
ref="pickHousehold"> <!-- to cast child method -->
</add-persons>
</li>
<li v-if="!hasHousehold">
<button @click="setModeNew" class="btn btn-sm btn-create">{{ $t('household_members_editor.household.create_household') }}</button>
</li>
@@ -77,16 +87,30 @@
import { mapGetters, mapState } from 'vuex';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
import CurrentHousehold from './CurrentHousehold';
import AddPersons from 'ChillPersonAssets/vuejs/_components/AddPersons';
export default {
name: 'Household',
components: {
AddPersons,
CurrentHousehold,
HouseholdRenderBox,
},
emits: ['readyToGo'],
data() {
return {
addPersons: {
key: 'household_find',
options: {
type: ['household'],
priority: null,
uniq: true,
button: {
size: 'btn-sm',
type: 'btn-search',
}
}
},
addAddress: {
key: 'household_new',
options: {
@@ -166,6 +190,13 @@ export default {
this.$store.dispatch('selectHousehold', h);
this.$emit('readyToGo');
},
pickHouseholdFound({selected, modal}) {
selected.forEach(function(item) {
this.selectHousehold(item.result);
}, this);
this.$refs.pickHousehold.resetSearch(); // to cast child method
modal.showModal = false;
},
removeHouseholdAddress() {
this.$store.commit('removeHouseholdAddress');
},

View File

@@ -0,0 +1,42 @@
<template>
<ckeditor
name="content"
v-bind:placeholder="$t('comment.content')"
:editor="editor"
v-model="content"
tag-name="textarea">
</ckeditor>
</template>
<script>
import CKEditor from '@ckeditor/ckeditor5-vue';
import ClassicEditor from "ChillMainAssets/module/ckeditor5";
export default {
name: "PersonComment.vue",
components: {
ckeditor: CKEditor.component,
},
props: ['conc'],
data() {
return {
editor: ClassicEditor,
}
},
computed: {
content: {
get() {
return this.$props.conc.comment || '';
},
set(value) {
console.log('set content', value);
this.$store.commit('setComment', {conc: this.$props.conc, comment: value})
}
}
}
}
</script>
<style scoped>
</style>

View File

@@ -3,15 +3,16 @@
<h2>{{ $t('household_members_editor.positioning.persons_to_positionnate')}}</h2>
<div class="list-household-members">
<div class="list-household-members flex-table">
<div
v-for="conc in concerned"
class="item-bloc"
v-bind:key="conc.person.id"
class="item-bloc"
v-bind:key="conc.person.id"
>
<div class="pick-position">
<div class="pick-position item-row">
<div class="person">
<person-render-box render="badge" :options="{}" :person="conc.person"></person-render-box>
<!-- <h3>{{ conc.person.text }}</h3> -->
<h3><person-text :person="conc.person"></person-text></h3>
</div>
<div class="holder">
<button
@@ -37,6 +38,12 @@
</button>
</div>
</div>
<div class="item-row">
<div>
<h6>{{ $t('household_members_editor.positioning.comment') }}</h6>
<person-comment :conc="conc"></person-comment>
</div>
</div>
</div>
</div>
</template>
@@ -46,12 +53,16 @@ import MemberDetails from './MemberDetails.vue';
import {mapGetters, mapState} from "vuex";
import CurrentHousehold from "./CurrentHousehold";
import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue';
import PersonComment from './PersonComment';
import PersonText from '../../_components/Entity/PersonText.vue';
export default {
name: "Positioning",
components: {
CurrentHousehold,
PersonRenderBox,
PersonComment,
PersonText
},
computed: {
...mapState([

View File

@@ -52,6 +52,7 @@ const appMessages = {
positioning: {
persons_to_positionnate: 'Usagers à positionner',
holder: "Titulaire",
comment: "Commentaire",
},
app: {
next: 'Suivant',
@@ -77,7 +78,12 @@ const appMessages = {
dates: {
start_date: "Début de validité",
end_date: "Fin de validité",
dates_title: "Période de validité",
dates_title: "Depuis le",
},
composition: {
composition: "Composition familiale",
household_composition: "Composition du ménage",
number_of_children: "Nombre d'enfants mineurs au sein du ménage",
},
confirmation: {
save: "Enregistrer",

View File

@@ -1,5 +1,6 @@
import { createStore } from 'vuex';
import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js';
import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods.js'
import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js';
import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js';
@@ -54,8 +55,11 @@ const store = createStore({
*/
householdSuggestionByAccompanyingPeriod: [], // TODO rename into householdsSuggestion
showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1,
householdCompositionType: null,
numberOfChildren: 0,
addressesSuggestion: [],
showAddressSuggestion: true,
householdCompositionTypes: [],
warnings: [],
errors: []
},
@@ -250,7 +254,8 @@ const store = createStore({
payload_conc,
payload = {
concerned: [],
destination: null
destination: null,
composition: null,
}
;
@@ -261,7 +266,6 @@ const store = createStore({
};
if (getters.isHouseholdNew && state.household.current_address !== null) {
console.log(state.household);
payload.destination.forceAddress = { id: state.household.current_address.address_id };
}
}
@@ -290,6 +294,19 @@ const store = createStore({
payload.concerned.push(payload_conc);
}
if (getters.isHouseholdNew) {
payload.composition = {
household_composition_type: {
type: state.householdCompositionType.type,
id: state.householdCompositionType.id,
},
number_of_children: state.numberOfChildren,
start_date: {
datetime: datetimeToISO(state.startDate),
},
};
}
return payload;
},
},
@@ -409,6 +426,15 @@ const store = createStore({
setErrors(state, errors) {
state.errors = errors;
},
setHouseholdCompositionTypes(state, types) {
state.householdCompositionTypes = types;
},
setHouseholdCompositionType(state, id) {
state.householdCompositionType = state.householdCompositionTypes.find(t => t.id = id);
},
setNumberOfChildren(state, number) {
state.numberOfChildren = Number.parseInt(number);
},
addAddressesSuggestion(state, addresses) {
let existingIds = state.addressesSuggestion
.map(a => a.address_id);
@@ -570,4 +596,8 @@ if (concerned.length > 0) {
});
}
fetchResults(`/api/1.0/person/houehold/composition/type.json`).then(types => {
store.commit('setHouseholdCompositionTypes', types);
})
export { store };

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,12 @@
<template>
<ul class="record_actions">
<ul class="record_actions">
<li class="add-persons">
<a class="btn" :class="getClassButton" :title="$t(buttonTitle)"
@click="openModal"><span v-if="displayTextButton">{{ $t(buttonTitle) }}</span></a>
<a class="btn" :class="getClassButton" :title="$t(buttonTitle)"
@click="openModal"><span v-if="displayTextButton">{{ $t(buttonTitle) }}</span></a>
</li>
</ul>
</ul>
<teleport to="body">
<teleport to="body">
<modal v-if="modal.showModal"
:modalDialogClass="modal.modalDialogClass"
@close="modal.showModal = false">
@@ -67,9 +67,10 @@
<div class="create-button">
<on-the-fly
v-if="query.length >= 3"
v-bind:buttonText="$t('onthefly.create.button', {q: query})"
:buttonText="$t('onthefly.create.button', {q: query})"
action="create"
@saveFormOnTheFly="saveFormOnTheFly">
@saveFormOnTheFly="saveFormOnTheFly"
:canCloseModal="canCloseOnTheFlyModal">
</on-the-fly>
</div>
@@ -92,8 +93,7 @@ import Modal from 'ChillMainAssets/vuejs/_components/Modal';
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import PersonSuggestion from './AddPersons/PersonSuggestion';
import { searchEntities } from 'ChillPersonAssets/vuejs/_api/AddPersons';
import { postPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly";
import { postThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly";
import { makeFetch } from 'ChillMainAssets/lib/api/apiMethods';
export default {
name: 'AddPersons',
@@ -121,7 +121,8 @@ export default {
suggested: [],
selected: [],
priorSuggestion: {}
}
},
canCloseOnTheFlyModal: false
}
},
computed: {
@@ -181,7 +182,7 @@ export default {
},
hasPriorSuggestion() {
return this.search.priorSuggestion.key ? true : false;
}
},
},
methods: {
openModal() {
@@ -195,18 +196,18 @@ export default {
setTimeout(function() {
if (query === "") {
this.loadSuggestions([]);
return;
this.loadSuggestions([]);
return;
}
if (query === this.search.query) {
if (this.currentSearchQueryController !== undefined) {
this.currentSearchQueryController.abort()
}
this.currentSearchQueryController = new AbortController();
searchEntities({ query, options: this.options }, this.currentSearchQueryController)
if (this.currentSearchQueryController !== undefined) {
this.currentSearchQueryController.abort()
}
this.currentSearchQueryController = new AbortController();
searchEntities({ query, options: this.options }, this.currentSearchQueryController)
.then(suggested => new Promise((resolve, reject) => {
this.loadSuggestions(suggested.results);
resolve();
this.loadSuggestions(suggested.results);
resolve();
}));
}
}.bind(this), query.length > 3 ? 300 : 700);
@@ -241,13 +242,12 @@ export default {
return item.result.type + item.result.id;
},
addPriorSuggestion() {
//console.log('addPriorSuggestion', this.hasPriorSuggestion);
// console.log('prior suggestion', this.priorSuggestion);
if (this.hasPriorSuggestion) {
console.log('addPriorSuggestion',);
// console.log('addPriorSuggestion',);
this.suggested.unshift(this.priorSuggestion);
this.selected.unshift(this.priorSuggestion);
console.log('reset priorSuggestion');
this.newPriorSuggestion(null);
}
},
@@ -261,6 +261,7 @@ export default {
}
this.search.priorSuggestion = suggestion;
// console.log('search priorSuggestion', this.search.priorSuggestion);
this.addPriorSuggestion(suggestion);
} else {
this.search.priorSuggestion = {};
}
@@ -268,23 +269,38 @@ export default {
saveFormOnTheFly({ type, data }) {
// console.log('saveFormOnTheFly from addPersons, type', type, ', data', data);
if (type === 'person') {
// console.log('type person with', data);
postPerson(data)
.then(person => new Promise((resolve, reject) => {
console.log('onthefly create: post person', person);
this.newPriorSuggestion(person);
resolve();
}));
makeFetch('POST', '/api/1.0/person/person.json', data)
.then(response => {
this.newPriorSuggestion(response);
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
else if (type === 'thirdparty') {
// console.log('type thirdparty with', data);
postThirdparty(data)
.then(thirdparty => new Promise((resolve, reject) => {
// console.log('onthefly create: post thirdparty', thirdparty);
this.newPriorSuggestion(thirdparty);
resolve();
}));
makeFetch('POST', '/api/1.0/thirdparty/thirdparty.json', data)
.then(response => {
this.newPriorSuggestion(response);
this.canCloseOnTheFlyModal = true;
})
.catch((error) => {
if (error.name === 'ValidationException') {
for (let v of error.violations) {
this.$toast.open({message: v });
}
} else {
this.$toast.open({message: 'An error occurred'});
}
})
}
this.canCloseOnTheFlyModal = false;
}
},
}

View File

@@ -26,6 +26,11 @@
v-if="item.result.type === 'user'"
v-bind:item="item">
</suggestion-user>
<suggestion-household
v-if="item.result.type === 'household'"
v-bind:item="item">
</suggestion-household>
</label>
</div>
@@ -35,6 +40,7 @@
import SuggestionPerson from './TypePerson';
import SuggestionThirdParty from './TypeThirdParty';
import SuggestionUser from './TypeUser';
import SuggestionHousehold from './TypeHousehold';
export default {
name: 'PersonSuggestion',
@@ -42,6 +48,7 @@ export default {
SuggestionPerson,
SuggestionThirdParty,
SuggestionUser,
SuggestionHousehold,
},
props: [
'item',
@@ -87,11 +94,11 @@ export default {
label {
display: inline-flex;
width: 100%;
div.container {
div.container:not(.household) {
& > input {
margin-right: 0.8em;
}
span:not(.name) {
> span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;

View File

@@ -0,0 +1,27 @@
<template>
<div class="container household">
<household-render-box :household="item.result" :isAddressMultiline="false"></household-render-box>
</div>
<div class="right_actions">
<badge-entity
:entity="item.result"
:options="{ displayLong: true }">
</badge-entity>
</div>
</template>
<script>
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
import HouseholdRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/HouseholdRenderBox.vue';
export default {
name: 'SuggestionHousehold',
components: {
BadgeEntity,
HouseholdRenderBox,
},
props: ['item'],
}
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div class="container">
<span class="name">
{{ item.result.text }}
<person-text :person="item.result"></person-text>
</span>
<span class="birthday" v-if="hasBirthdate">
{{ $d(item.result.birthdate.datetime, 'short') }}
@@ -28,12 +28,14 @@
<script>
import OnTheFly from 'ChillMainAssets/vuejs/OnTheFly/components/OnTheFly.vue';
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: 'SuggestionPerson',
components: {
OnTheFly,
BadgeEntity
BadgeEntity,
PersonText,
},
props: ['item'],
computed: {

View File

@@ -2,9 +2,9 @@
<div class="container tpartycontainer">
<div class="tparty-identification">
<span class="name">
{{ item.result.text }}
{{ item.result.text }}&nbsp;
</span>
<span class="location">
<span class="location">
<template v-if="hasAddress">
{{ getAddress.text }} -
{{ getAddress.postcode.name }}
@@ -100,5 +100,13 @@ export default {
font-variant: all-small-caps;
}
}
.tparty-identification {
span:not(.name) {
margin-left: 0.5em;
opacity: 0.5;
font-size: 90%;
font-style: italic;
}
}
}
</style>

View File

@@ -1,14 +1,15 @@
<template>
<div v-if="render === 'bloc'" class="item-bloc">
<div v-if="render === 'bloc'" class="item-bloc">
<section class="chill-entity entity-person">
<div class="item-row entity-bloc">
<div class="item-row entity-bloc">
<div class="item-col">
<div class="entity-label">
<div :class="'denomination h' + options.hLevel">
<a v-if="options.addLink === true" :href="getUrl">
<a v-if="options.addLink === true" :href="getUrl">
<!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
@@ -16,19 +17,20 @@
</span>
</a>
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.deathdate" class="deathdate"> ()</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
<span :class="'altname altname-' + altNameKey">{{ altNameLabel }}</span>
</span>
<!-- use person-text here to avoid code duplication ? TODO -->
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.deathdate" class="deathdate"> ()</span>
<span v-if="person.altNames && options.addAltNames == true" class="altnames">
<span :class="'altname altname-' + altNameKey">{{ altNameLabel }}</span>
</span>
<span v-if="options.addId == true" class="id-number" :title="'n° ' + person.id">{{ person.id }}</span>
<span v-if="options.addId == true" class="id-number" :title="'n° ' + person.id">{{ person.id }}</span>
<badge-entity v-if="options.addEntity === true"
:entity="person"
:options="{ displayLong: options.entityDisplayLong }">
</badge-entity>
<badge-entity v-if="options.addEntity === true"
:entity="person"
:options="{ displayLong: options.entityDisplayLong }">
</badge-entity>
</div>
@@ -47,96 +49,96 @@
{{ $t('renderbox.deathdate') + ' ' + deathdate }}
</time>
<span v-if="options.addAge && person.birthdate" class="age">{{ getAge }} {{ $t('renderbox.years_old')}}</span>
<span v-if="options.addAge && person.birthdate" class="age">{{ $tc('renderbox.years_old', person.age) }}</span>
</p>
</div>
</div>
<div class="item-col">
<div class="float-button bottom">
<div class="box">
<div class="action">
<slot name="record-actions"></slot>
</div>
<ul class="list-content fa-ul">
<div class="float-button bottom">
<div class="box">
<div class="action">
<slot name="record-actions"></slot>
</div>
<ul class="list-content fa-ul">
<li v-if="person.current_household_id">
<i class="fa fa-li fa-map-marker"></i>
<address-render-box v-if="person.current_household_address"
:address="person.current_household_address"
:isMultiline="isMultiline">
</address-render-box>
<p v-else class="chill-no-data-statement">
{{ $t('renderbox.household_without_address') }}
</p>
<a v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl"
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
<span class="badge rounded-pill bg-chill-beige">
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
</span>
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker"></i>
<p class="chill-no-data-statement">
{{ $t('renderbox.no_data') }}
</p>
</li>
<li v-if="person.current_household_id">
<i class="fa fa-li fa-map-marker"></i>
<address-render-box v-if="person.current_household_address"
:address="person.current_household_address"
:isMultiline="isMultiline">
</address-render-box>
<p v-else class="chill-no-data-statement">
{{ $t('renderbox.household_without_address') }}
</p>
<a v-if="options.addHouseholdLink === true"
:href="getCurrentHouseholdUrl"
:title="$t('persons_associated.show_household_number', {id: person.current_household_id})">
<span class="badge rounded-pill bg-chill-beige">
<i class="fa fa-fw fa-home"></i><!--{{ $t('persons_associated.show_household') }}-->
</span>
</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-map-marker"></i>
<p class="chill-no-data-statement">
{{ $t('renderbox.no_data') }}
</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + person.mobilenumber">{{ person.mobilenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.phonenumber">
<i class="fa fa-li fa-phone"></i>
<a :href="'tel: ' + person.phonenumber">{{ person.phonenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.mobilenumber">
<i class="fa fa-li fa-mobile"></i>
<a :href="'tel: ' + person.mobilenumber">{{ person.mobilenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-mobile"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.phonenumber">
<i class="fa fa-li fa-phone"></i>
<a :href="'tel: ' + person.phonenumber">{{ person.phonenumber }}</a>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-phone"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<slot name="custom-zone"></slot>
<li v-if="person.centers !== undefined && person.centers.length > 0 && options.addCenter">
<i class="fa fa-li fa-long-arrow-right"></i>
<template v-for="c in person.centers">{{ c.name }}</template>
</li>
<li v-else-if="options.addNoData">
<i class="fa fa-li fa-long-arrow-right"></i>
<p class="chill-no-data-statement">{{ $t('renderbox.no_data') }}</p>
</li>
<slot name="custom-zone"></slot>
</ul>
</div>
</div>
</div>
</ul>
</div>
</div>
</div>
</div>
</section>
</div>
</div>
<span v-if="render === 'badge'" class="chill-entity entity-person badge-person">
<a v-if="options.addLink === true" :href="getUrl">
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{{ person.text }}
<span v-if="render === 'badge'" class="chill-entity entity-person badge-person">
<a v-if="options.addLink === true" :href="getUrl">
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
<person-text :person="person"></person-text>
</a>
<span v-else>
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
{{ person.text }}
<span v-if="options.isHolder" class="fa-stack fa-holder" :title="$t('renderbox.holder')">
<i class="fa fa-circle fa-stack-1x text-success"></i>
<i class="fa fa-stack-1x">T</i>
</span>
<person-text :person="person"></person-text>
</span>
<slot name="post-badge"></slot>
</span>
</span>
</template>
@@ -145,13 +147,15 @@ import {dateToISO} from 'ChillMainAssets/chill/js/date.js';
import AddressRenderBox from 'ChillMainAssets/vuejs/_components/Entity/AddressRenderBox.vue';
import Confidential from 'ChillMainAssets/vuejs/_components/Confidential.vue';
import BadgeEntity from 'ChillMainAssets/vuejs/_components/BadgeEntity.vue';
import PersonText from 'ChillPersonAssets/vuejs/_components/Entity/PersonText.vue';
export default {
name: "PersonRenderBox",
components: {
AddressRenderBox,
Confidential,
BadgeEntity
BadgeEntity,
PersonText
},
props: ['person', 'options', 'render', 'returnPath'],
computed: {
@@ -166,7 +170,7 @@ export default {
return this.person.gender === 'woman' ? 'fa-venus' : this.person.gender === 'man' ? 'fa-mars' : this.person.gender === 'neuter' ? 'fa-neuter' : 'fa-genderless';
},
getGenderTranslation: function() {
return this.person.gender === 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
return this.person.gender === 'woman' ? 'renderbox.birthday.woman' : 'renderbox.birthday.man';
},
getGender() {
return this.person.gender === 'woman' ? 'person.gender.woman' : this.person.gender === 'man' ? 'person.gender.man' : this.person.gender === 'neuter' ? 'person.gender.neuter' : 'person.gender.undefined';
@@ -198,24 +202,6 @@ export default {
getUrl: function() {
return `/fr/person/${this.person.id}/general`;
},
getAge: function() {
// TODO only one abstract function
if(this.person.birthdate && !this.person.deathdate){
const birthday = new Date(this.person.birthdate.datetime)
const now = new Date()
return (now.getFullYear() - birthday.getFullYear())
} else if(this.person.birthdate && this.person.deathdate){
const birthday = new Date(this.person.birthdate.datetime)
const deathdate = new Date(this.person.deathdate.datetime)
return (deathdate.getFullYear() - birthday.getFullYear())
} else if(!this.person.birthdate && this.person.deathdate.datetime) {
// todo: change this
return "Age unknown"
} else {
// todo: change this
return "Age unknown"
}
},
getCurrentHouseholdUrl: function() {
let returnPath = this.returnPath ? `?returnPath=${this.returnPath}` : ``;
return `/fr/person/household/${this.person.current_household_id}/summary${returnPath}`

View File

@@ -0,0 +1,50 @@
<template>
<span v-if="isCut">{{ cutText }}</span>
<span v-else class="person-text">
<span class="firstname">{{ person.firstName }}</span>
<span class="lastname">{{ person.lastName }}</span>
<span v-if="person.altNames && person.altNames.length > 0" class="altnames">
<span :class="'altname altname-' + altNameKey"> ({{ altNameLabel }})</span>
</span>
<span class="age" v-if="this.addAge && person.birthdate !== null && person.deathdate === null">{{ $tc('renderbox.years_old', person.age) }}</span>
<span v-else-if="this.addAge && person.deathdate !== null">&nbsp;()</span>
</span>
</template>
<script>
export default {
name: "PersonText",
props: {
person: {
required: true,
},
isCut: {
type: Boolean,
required: false,
default: false
},
addAge: {
type: Boolean,
required: false,
default: true,
}
},
computed: {
altNameLabel: function() {
for(let i = 0; i < this.person.altNames.length; i++){
return this.person.altNames[i].label
}
},
altNameKey: function() {
for(let i = 0; i < this.person.altNames.length; i++){
return this.person.altNames[i].key
}
},
cutText: function() {
let more = (this.person.text.length > 15) ?'…' : '';
return this.person.text.slice(0,15) + more;
}
}
}
</script>

View File

@@ -22,24 +22,45 @@
<div v-else-if="action === 'edit' || action === 'create'">
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="lastname" v-model="lastName" v-bind:placeholder="$t('person.lastname')" />
<input
class="form-control form-control-lg"
id="lastname"
v-model="lastName"
:placeholder="$t('person.lastname')"
@change="checkErrors"
/>
<label for="lastname">{{ $t('person.lastname') }}</label>
</div>
<div class="form-floating mb-3">
<input class="form-control form-control-lg" id="firstname" v-model="firstName" v-bind:placeholder="$t('person.firstname')" />
<input
class="form-control form-control-lg"
id="firstname"
v-model="firstName"
:placeholder="$t('person.firstname')"
@change="checkErrors"
/>
<label for="firstname">{{ $t('person.firstname') }}</label>
</div>
<div v-for="(a) in config.altNames" :key="a.key" class="form-floating mb-3">
<input class="form-control form-control-lg" :id="a.key" @input="onAltNameInput" />
<input
class="form-control form-control-lg"
:id="a.key"
@input="onAltNameInput"
/>
<label :for="a.key">{{ a.labels.fr }}</label>
</div>
<!-- TODO fix placeholder if undefined
-->
<div class="form-floating mb-3">
<select class="form-select form-select-lg" id="gender" v-model="gender">
<select
class="form-select form-select-lg"
id="gender"
v-model="gender"
@change="checkErrors"
>
<option selected disabled >{{ $t('person.gender.placeholder') }}</option>
<option value="woman">{{ $t('person.gender.woman') }}</option>
<option value="man">{{ $t('person.gender.man') }}</option>
@@ -62,8 +83,8 @@
<span class="input-group-text" id="phonenumber"><i class="fa fa-fw fa-phone"></i></span>
<input class="form-control form-control-lg"
v-model="phonenumber"
v-bind:placeholder="$t('person.phonenumber')"
v-bind:aria-label="$t('person.phonenumber')"
:placeholder="$t('person.phonenumber')"
:aria-label="$t('person.phonenumber')"
aria-describedby="phonenumber" />
</div>
@@ -71,8 +92,8 @@
<span class="input-group-text" id="mobilenumber"><i class="fa fa-fw fa-mobile"></i></span>
<input class="form-control form-control-lg"
v-model="mobilenumber"
v-bind:placeholder="$t('person.mobilenumber')"
v-bind:aria-label="$t('person.mobilenumber')"
:placeholder="$t('person.mobilenumber')"
:aria-label="$t('person.mobilenumber')"
aria-describedby="mobilenumber" />
</div>
@@ -80,11 +101,17 @@
<span class="input-group-text" id="email"><i class="fa fa-fw fa-at"></i></span>
<input class="form-control form-control-lg"
v-model="email"
v-bind:placeholder="$t('person.email')"
v-bind:aria-label="$t('person.email')"
:placeholder="$t('person.email')"
:aria-label="$t('person.email')"
aria-describedby="email" />
</div>
<div class="alert alert-warning" v-if="errors.length">
<ul>
<li v-for="(e, i) in errors" :key="i">{{ e }}</li>
</ul>
</div>
</div>
</template>
@@ -108,6 +135,7 @@ export default {
config: {
altNames: []
},
errors: []
}
},
computed: {
@@ -183,6 +211,18 @@ export default {
}
},
methods: {
checkErrors(e) {
this.errors = [];
if (!this.person.lastName) {
this.errors.push("Le nom ne doit pas être vide.");
}
if (!this.person.firstName) {
this.errors.push("Le prénom ne doit pas être vide.");
}
if (!this.person.gender) {
this.errors.push("Le genre doit être renseigné");
}
},
loadData() {
getPerson(this.id)
.then(person => new Promise((resolve, reject) => {

View File

@@ -7,7 +7,7 @@
{% import '@ChillPerson/AccompanyingCourse/Comment/macro_showItem.html.twig' as m %}
{% macro recordAction(comment, isPinned) %}
{% if isPinned is defined and isPinned == 'true' %}
{% if isPinned is defined and isPinned == true %}
{% else %}
<li>
<form method="post" action="{{ chill_path_forward_return_path('chill_person_accompanying_period_comment_pin', {'id': comment.id}) }}">
@@ -66,8 +66,8 @@
{{ _self.form_comment('edit', edit_form) }}
{% else %}
{{ m.show_comment(accompanyingCourse.pinnedComment, {
'pinned': 'true',
'recordAction': _self.recordAction(accompanyingCourse.pinnedComment, 'true')
'pinned': true,
'recordAction': _self.recordAction(accompanyingCourse.pinnedComment, true)
}) }}
{% endif %}
{% endif %}

View File

@@ -3,7 +3,8 @@
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: type, id: entity.id },
buttonText: entity|chill_entity_render_string
buttonText: entity|chill_entity_render_string,
isDead: entity.deathdate is not null
} %}
{% endmacro %}

View File

@@ -1,23 +1,25 @@
{%- set countPersonLocation = accompanyingCourse.availablePersonLocation|length -%}
{%- set hasPersonLocation = countPersonLocation > 0 -%}
<div class="alert alert-danger {% if hasPersonLocation %}alert-with-actions{% endif %}">
<div class="float-button bottom"><div class="box">
<div class="action">
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
{{ 'fix it'|trans }}
</a>
</li>
</ul>
<div class="float-button bottom">
<div class="box">
<div class="action">
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_accompanying_course_edit', { 'accompanying_period_id': accompanyingCourse.id, '_fragment': 'section-20' }) }}">
<i class="fa fa-fw fa-crosshairs"></i>
{{ 'fix it'|trans }}
</a>
</li>
</ul>
</div>
<p>
{{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}</p>
{% if not hasPersonLocation %}
<p>
{{ 'Associate at least one member with an household, and set an address to this household'|trans }}</p>
{% endif %}
</div>
<p>
{{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}</p>
{% if not hasPersonLocation %}
<p>
{{ 'Associate at least one member with an household, and set an address to this household'|trans }}</p>
{% endif %}
</div></div>
</div>
</div>

View File

@@ -1,5 +1,7 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% block title %}{{ 'Close accompanying course'|trans }}{% endblock %}
{% block content %}
<h1>{{ "Close accompanying course"|trans }}</h1>

View File

@@ -9,6 +9,7 @@
action: 'show', displayBadge: true,
targetEntity: { name: type, id: entity.id },
buttonText: entity|chill_entity_render_string,
isDead: entity.deathdate is defined and entity.deathdate is not null,
parent: parent
} %}
{% endmacro %}
@@ -76,7 +77,10 @@
<h4 class="item-key visually-hidden">{{ 'Pinned comment'|trans }}</h4>
<blockquote class="chill-user-quote">
<i class="fa fa-flag float-end text-chill-gray" title="{{ 'pinned'|trans }}"></i>
{{ accompanyingCourse.pinnedComment.content|chill_markdown_to_html }}
{{ accompanyingCourse.pinnedComment.content|u.truncate(250, '…', false)|chill_markdown_to_html }}
{% if accompanyingCourse.pinnedComment.content|length > 250 %}
<a href="{{ chill_path_add_return_path('chill_person_accompanying_period_comment_list', {'accompanying_period_id': accompanyingCourse.id}) }}">{{ 'Read more'|trans }}</a>
{% endif %}
<div class="metadata">
{{ 'Last updated by'| trans }}
<span class="user">
@@ -131,10 +135,10 @@
{% if accompanyingCourse.scopes is not empty %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="scopes">
<h4 class="item-key">{{ 'Scopes'|trans }}</h4>
<h4 class="item-key">{{ 'Concerned scopes'|trans }}</h4>
<div>
{% for s in accompanyingCourse.scopes %}
<span>{{ s.name|localize_translatable_string|capitalize }}</span>{% if not loop.last %}, {% endif %}
<span>{{ s.name|localize_translatable_string|upper }}</span>{% if not loop.last %}, {% endif %}
{% endfor %}
</div>
</div>
@@ -147,14 +151,14 @@
{% if accompanyingCourse.requestorPerson is not null %}
<h4 class="item-key">{{ 'Requestor'|trans }}</h4>
{% if accompanyingCourse.requestorAnonymous %}
<div class="confidential"><p class="blur">{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}</p></div>
<div class="confidential"><p>{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}</p></div>
{% else %}
{{ _self.insert_onthefly('person', accompanyingCourse.requestorPerson) }}
{% endif %}
{% elseif accompanyingCourse.requestorThirdParty is not null %}
<h4 class="item-key">{{ 'Requestor'|trans }}</h4>
{% if accompanyingCourse.requestorAnonymous %}
<div class="confidential"><p class="blur">{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}</p></div>
<div class="confidential"><p>{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}</p></div>
{% else %}
{{ _self.insert_onthefly('thirdparty', accompanyingCourse.requestorThirdParty) }}
{% endif %}
@@ -180,7 +184,25 @@
</div>
</div>
{% endif %}
<div class="mbloc col col-sm-6 col-lg-4">
<div class="notification-counter">
<h4 class="item-key">{{ 'notification.Notifications'|trans }}</h4>
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
{% if notif_counter.total > 0 %}
<div class="my-2">
<a href="#notification-list">
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
</a>
</div>
{% endif %}
<div class="d-grid gap-2">
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
{{ 'notification.Notify'|trans }}
</a>
</div>
</div>
</div>
</div>
<div class="social-actions my-4">
@@ -206,21 +228,16 @@
{% include 'ChillActivityBundle:Activity:list_recent.html.twig' with { 'context': 'accompanyingCourse', 'no_action': true } %}
</div>
{% endblock %}
<div class="notification notification-list">
{% set notifications = chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) %}
{% if notifications is not empty %}
{{ notifications|raw }}
{% endif %}
</div>
</div>
{% endblock %}
{% block block_post_menu %}
<div class="post-menu pt-4">
<div class="d-grid gap-2">
<a class="btn btn-primary" href="{{ chill_path_add_return_path('chill_main_notification_create', {'entityClass': 'Chill\\PersonBundle\\Entity\\AccompanyingPeriod', 'entityId': accompanyingCourse.id}) }}">
<i class="fa fa-paper-plane fa-fw"></i>
{{ 'notification.Notify'|trans }}
</a>
</div>
{{ chill_list_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', accompanyingCourse.id) }}
</div>
<div class="post-menu pt-4"></div>
{% endblock %}

View File

@@ -0,0 +1,29 @@
{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %}
{% block title 'Re-Open a period'|trans %}
{% block content %}
<h1>{{ "Re-Open a period"|trans }}</h1>
<p class="message-confirm">{{ 'Are you sure you want to re-open this period ?'|trans }}<p>
{{ form_start(form) }}
{% set accompanying_course_id = null %}
{% if accompanyingCourse %}
{% set accompanying_course_id = accompanyingCourse.id %}
{% endif %}
<ul class="record_actions sticky-form-buttons">
<li class="cancel">
<a href="{{ chill_return_path_or('chill_person_accompanying_course_index', {'accompanying_period_id' : accompanyingCourse.id}) }}" class="btn btn-cancel">
{{ 'Return'|trans }}
</a>
</li>
<li>
<button class="btn btn-update" type="submit">{{ 'Save'|trans }}</button>
</li>
</ul>
{{ form_end(form) }}
{% endblock %}

View File

@@ -0,0 +1,127 @@
<div class="item-bloc accompanying_course_work-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-row">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">{{ w.socialAction|chill_entity_render_string }}
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ w.startDate|format_date('short') }}</b>
</li>
{% if w.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ w.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
</span>
</h2>
</div>
<div class="item-row separator">
<div class="wrap-list">
{% if w.createdBy %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Referrer'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item">
{{ w.createdBy|chill_entity_render_box }}
</p>
</div>
</div>
{% endif %}
{%- if w.persons -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Persons in accompanying course'|trans }}</h3>
</div>
<div class="wl-col list">
{% for p in w.persons %}
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: p.id },
buttonText: p|chill_entity_render_string,
isDead: p.deathdate is not null
} %}
</span>
{% endfor %}
</div>
</div>
{% endif %}
{%- if w.handlingThierParty -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Thirdparty handling'|trans }}</h3>
</div>
<div class="wl-col list">
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'thirdparty', id: w.handlingThierParty.id },
buttonText: w.handlingThierParty|chill_entity_render_string
} %}
</span>
</div>
</div>
{% endif %}
{%- if w.socialAction.issue -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Social issue'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item social-issues">
{{ w.socialAction.issue|chill_entity_render_box }}
</p>
</div>
</div>
{% endif %}
</div>
</div>
<div class="item-row column">
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' %}
</div>
<div class="item-row separator">
<div class="item-col item-meta">
{% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) %}
{% if notif_counter.total > 0 %}
{{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) }}
{% endif %}
{% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %}
{{ macro.updatedBy(w) }}
</div>
{% if displayAction is defined and displayAction == true %}
<div class="item-col">
<ul class="record_actions">
<li>
<a class="btn btn-edit" title="{{ 'Edit'|trans }}"
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>
</ul>
</div>
{% endif %}
</div>
</div>

View File

@@ -2,27 +2,29 @@
{% block title 'accompanying_course_work.Edit accompanying course work'|trans %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('vue_accourse_work_edit') }}
{{ encore_entry_link_tags('mod_entity_workflow_pick') }}
{% endblock %}
{% block content %}
<div class="accompanying_course_work-edit">
<h1>{{ block('title') }}</h1>
<div id="accompanying_course_work_edit"></div>
</div>
{% endblock %}
{% block block_post_menu %}
<div class="post-menu pt-4"></div>
{% endblock %}
{% block js %}
{{ parent() }}
<script type="text/javascript">
window.accompanyingCourseWork = {{ json|json_encode|raw }};
window.accompanyingCourseWork = {{ json|json_encode|raw }};
</script>
{{ encore_entry_script_tags('vue_accourse_work_edit') }}
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
{% endblock %}
{% block css %}
{{ parent() }}
{{ encore_entry_link_tags('vue_accourse_work_edit') }}
{% endblock %}

View File

@@ -18,130 +18,7 @@
{% else %}
<div class="flex-table accompanying_course_work-list">
{% for w in works %}
<div class="item-bloc">
<div class="item-row">
<h2 class="badge-title">
<span class="title_label"></span>
<span class="title_action">{{ w.socialAction|chill_entity_render_string }}
<ul class="small_in_title columns mt-1">
<li>
<span class="item-key">{{ 'accompanying_course_work.start_date'|trans ~ ' : ' }}</span>
<b>{{ w.startDate|format_date('short') }}</b>
</li>
{% if w.endDate %}
<li>
<span class="item-key">{{ 'accompanying_course_work.end_date'|trans ~ ' : ' }}</span>
<b>{{ w.endDate|format_date('short') }}</b>
</li>
{% endif %}
</ul>
</span>
</h2>
</div>
<div class="item-row separator">
<div class="wrap-list">
{% if w.createdBy %}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Referrer'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item">
{{ w.createdBy|chill_entity_render_box }}
</p>
</div>
</div>
{% endif %}
{%- if w.persons -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Persons in accompanying course'|trans }}</h3>
</div>
<div class="wl-col list">
{% for p in w.persons %}
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: p.id },
buttonText: p|chill_entity_render_string
} %}
</span>
{% endfor %}
</div>
</div>
{% endif %}
{%- if w.handlingThierParty -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Thirdparty handling'|trans }}</h3>
</div>
<div class="wl-col list">
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'thirdparty', id: w.handlingThierParty.id },
buttonText: w.handlingThierParty|chill_entity_render_string,
parent: {
'type': 'accompanying_period_resource',
'id': r.id,
'comment': r.comment,
'parent': {
'type': 'accompanying_period',
'id': accompanyingCourse.id
}
} %}
</span>
</div>
</div>
{% endif %}
{%- if w.socialAction.issue -%}
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Social issue'|trans }}</h3>
</div>
<div class="wl-col list">
<p class="wl-item social-issues">
{{ w.socialAction.issue|chill_entity_render_box }}
</p>
</div>
</div>
{% endif %}
</div>
</div>
<div class="item-row column">
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' with {} %}
</div>
<div class="item-row separator">
<div class="updatedBy">
{{ 'Last updated by'|trans}} <b>{{ w.updatedBy|chill_entity_render_box }}</b>,<br>
{{ 'le ' ~ w.updatedAt|format_datetime('long', 'short') }}
</div>
<ul class="record_actions">
<li>
<a class="btn btn-edit" title="{{ 'Edit'|trans }}"
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>
</ul>
</div>
</div>
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'displayAction': true } %}
{% endfor %}
</div>
{% endif %}

View File

@@ -31,7 +31,7 @@
<li class="associated-persons">
<span class="item-key">{{ 'Participants'|trans ~ ' : ' }}</span>
{% for p in w.persons %}
<span class="badge-person">{{ p|chill_entity_render_box }}</span>
<span class="badge-person">{{ p|chill_entity_render_box({'addAgeBadge': true}) }}</span>
{% endfor %}
</li>
</ul>
@@ -46,16 +46,15 @@
{% include 'ChillPersonBundle:AccompanyingCourseWork:_objectifs_results_evaluations.html.twig' with {} %}
</ul>
<div class="metadata text-end" style="font-size: 60%">
{{ 'Last updated by'|trans }}
<span class="user">{{ w.updatedBy|chill_entity_render_box }}</span>:
<span class="date">{{ w.updatedAt|format_datetime('short', 'short') }}</span>
<div class="metadata text-end">
{% import '@ChillPerson/Macro/updatedBy.html.twig' as macro %}
{{ macro.updatedBy(w) }}
</div>
</span>
</div>
</a>{# {{ dump(w) }} #}
</a>
{% endfor %}
</div>

View File

@@ -1,4 +1,4 @@
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass }}{% endif %}">
<div class="item-bloc accompanying-period-item{% if itemBlocClass is defined %} {{ itemBlocClass|raw }}{% endif %}" data-accompanying-period-id="{{ period.id|e('html_attr') }}">
<div class="item-row">
<div class="wrap-header">
<div class="wh-row">
@@ -11,7 +11,7 @@
<span class="badge rounded-pill bg-danger">{{- 'Emergency'|trans|upper -}}</span>
{% endif %}
{% if period.confidential %}
<span class="badge rounded-pill bg-danger">{{- 'Confidential'|trans|upper -}}</span>
<span class="badge rounded-pill bg-confidential">{{- 'Confidential'|trans|upper -}}</span>
{% endif %}
</div>
<div class="wh-col">
@@ -43,11 +43,12 @@
</div>
<div class="wh-col">
{% if chill_accompanying_periods.fields.user == 'visible' %}
{# the tags `data-referrer-text` is used by module `@ChillPerson/mod/AccompanyingPeriod/setReferrer.js` #}
{% if period.user %}
<abbr class="referrer" title="{{ 'Referrer'|trans }}">{{ 'Referrer'|trans }}:</abbr>
{{ period.user.username|chill_entity_render_box }}
<span data-referrer-text="data-referrer-text">{{ period.user|chill_entity_render_box }}</span>
{% else %}
<span class="chill-no-data-statement">{{ 'No accompanying user'|trans }}</span>
<span class="chill-no-data-statement" data-referrer-text="data-referrer-text">{{ 'No accompanying user'|trans }}</span>
{% endif %}
{% endif %}
</div>
@@ -65,7 +66,8 @@
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: period.requestorPerson.id },
buttonText: period.requestorPerson|chill_entity_render_string
buttonText: period.requestorPerson|chill_entity_render_string,
isDead: period.requestorPerson.deathdate is not null
} %}
</span>
{% endif %}
@@ -90,7 +92,8 @@
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: p.person.id },
buttonText: p.person|chill_entity_render_string
buttonText: p.person|chill_entity_render_string,
isDead: p.person.deathdate is not null
} %}
</span>
{% endfor %}
@@ -111,11 +114,19 @@
{% endif %}
</div>
</div>
{% if recordAction is defined %}
<div class="item-row separator">
<ul class="record_actions">
{{ recordAction }}
</ul>
<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>
{% endif %}
<div class="item-col">
{% if recordAction is defined %}
<ul class="record_actions">
{{ recordAction }}
</ul>
{% endif %}
</div>
</div>
</div>

View File

@@ -0,0 +1,30 @@
{% extends "@ChillMain/layout.html.twig" %}
{% set activeRouteKey = 'chill_person_accompanying_period_user_list' %}
{% block title %}{{ 'My accompanying periods'|trans }}{% endblock title %}
{% macro recordAction(period) %}
<li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': period.id }) }}"
class="btn btn-show" title="{{ 'See accompanying period'|trans }}"></a>
</li>
{% endmacro %}
{% block content %}
<div class="col-md-10">
<h1>{{ 'My accompanying periods'|trans }}</h1>
<div class="flex-table accompanyingcourse-list">
{% for period in accompanyingPeriods %}
{% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, 'recordAction': _self.recordAction(period)} %}
{% endfor %}
</div>
{{ chill_pagination(pagination) }}
</div>
{% endblock %}

View File

@@ -11,6 +11,7 @@
* addCenter bool
* hLevel integer
* addDeath bool
* addAgeBadge bool
* address_multiline bool
* customButtons [
'before' Twig\Markup, (injected with macro)
@@ -40,6 +41,11 @@
{%- endfor -%}
</span>
{%- endif -%}
{%- if options['addAgeBadge'] -%}
{% if person.age is not null and person.deathDate is null %}
<span>({{- 'years_old'|trans({ 'age': person.age }) -}})</span>
{% endif %}
{% endif %}
{% endmacro raw %}
{% macro label(person, options) %}
@@ -90,7 +96,7 @@
{#- must be on one line to avoid spaces with dash -#}
<time datetime="{{ person.deathdate|date('Y-m-d') }}" title="{{ 'deathdate'|trans }}">{{ person.deathdate|format_date("medium") }}</time>
{%- if options['addAge'] -%}
<span class="age">{{ 'years_old'|trans({ 'age': person.age }) }}</span>
<span class="age">&nbsp;{{ 'years_old'|trans({ 'age': person.age }) }}</span>
{%- endif -%}
{%- elseif person.birthdate is not null -%}
<time datetime="{{ person.birthdate|date('Y-m-d') }}" title="{{ 'Birthdate'|trans }}">

View File

@@ -14,6 +14,7 @@
'render': 'label',
'addLink': true,
'addInfo': true,
'addAgeBadge': true,
'customArea': {
'afterLabel': _self.addHolder(member.holder)
}

View File

@@ -4,12 +4,14 @@
{% block title 'household.Edit household members'|trans %}
{% block content %}
<div class="col-md-10 col-xxl household-members">
<div class="row">
<div class="col-md-10 col-xxl household-members">
<h1>{{ block('title') }}</h1>
<div id="household_members_editor"></div>
<h1>{{ block('title') }}</h1>
<div id="household_members_editor"></div>
</div>
</div>
</div>
{% endblock %}
{% block js %}

View File

@@ -51,7 +51,42 @@
</div>
<div class="item-bloc col-7 col-comment">
{% if form is null %}
{% set currentComposition = household.currentComposition %}
{% if currentComposition is not null %}
<div>
<h6>
{{ currentComposition.householdCompositionType.label|localize_translatable_string }}
</h6>
<p>
{{ 'household_composition.numberOfChildren children in household'|trans({'numberOfChildren': currentComposition.numberOfChildren}) }}
</p>
<p>
{{ 'household_composition.Since'|trans({'startDate': currentComposition.startDate}) }}
</p>
<ul class="record_actions">
<li>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_household_composition_index', {'id': household.id}) }}">
{{ 'household_composition.Update composition'|trans }}
</a>
</li>
</ul>
</div>
{% else %}
<div class="alert alert-danger">
<p>
{{ 'household_composition.Currently no composition'|trans }}
</p>
<ul class="record_actions" style="margin-bottom: 0">
<li>
<a class="btn btn-sm btn-update change-icon"
href="{{ path('chill_person_household_composition_index', {'id': household.id}) }}">
{{ 'household_composition.Add a composition'|trans }}
</a>
</li>
</ul>
</div>
{% endif %}
{% if household.waitingForBirth or not household.commentMembers.isEmpty() %}
<div class="p-4 bg-light">
{% if household.waitingForBirth %}
@@ -176,13 +211,26 @@
<span class="unfolded text-secondary">{{ 'household.Hide memberships'|trans }}</span>
</button>
</h2>
{% macro buttonsOldMembers(member) %}
{% set household = member.person.getCurrentHousehold %}
{% if household is not null %}
<li>
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a>
</li>
{% endif %}
{% endmacro %}
<div id="collapse_{{ p == '_none' ? '_none' : p.id }}"
class="accordion-collapse collapse"
aria-labelledby="heading_{{ p == '_none' ? '_none' : p.id }}"
data-bs-parent="#nonCurrent">
<div class="flex-table my-0 list-household-members">
{% for m in old_members %}
{% include '@ChillPerson/Household/_render_member.html.twig' with { 'member': m } %}
{% include '@ChillPerson/Household/_render_member.html.twig' with {
'member': m,
'customButtons': { 'before': _self.buttonsOldMembers(m) }
} %}
{% endfor %}
</div>
</div>

View File

@@ -0,0 +1,38 @@
{% extends '@ChillPerson/Household/layout.html.twig' %}
{% set activeRouteKey = 'chill_person_household_composition_index' %}
{% block title 'Remove household composition'|trans %}
{% block display_content %}
<p>{{ 'Concerns household n°%id%'|trans({ '%id%' : household.id } ) }}</p>
<div class="wl-row">
<div class="wl-col title">
<h3>{{ 'Composition'|trans }}:</h3>
</div>
<div class="wl-col list">
{% for m in household.members %}
<span class="wl-item">
{% include '@ChillMain/OnTheFly/_insert_vue_onthefly.html.twig' with {
action: 'show', displayBadge: true,
targetEntity: { name: 'person', id: m.person.id },
buttonText: m.person|chill_entity_render_string,
isDead: m.person.deathdate is not null
} %}
</span>
{% endfor %}
</div>
</div>
{% endblock %}
{% block content %}
{{ include('@ChillMain/Util/confirmation_template.html.twig',
{
'title' : 'Remove household composition'|trans,
'confirm_question' : 'Are you sure you want to remove this composition?'|trans,
'display_content' : block('display_content'),
'cancel_route' : 'chill_person_household_composition_index',
'cancel_parameters' : { 'composition_id' : composition.id, 'id' : household.id },
'form' : form
} ) }}
{% endblock %}

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