diff --git a/CHANGELOG.md b/CHANGELOG.md index df4dab422..542c99a37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to * [activity][export] DX/Feature: use of an `ActivityTypeRepositoryInterface` instead of the old-style EntityRepository * [person][export] Fixed: some inconsistency with date filter on accompanying courses * [person][export] Fixed: use left join for related entities in accompanying course aggregators +* [workflow] Feature: allow user to copy and send manually the access link for the workflow +* [workflow] Feature: show the email addresses that received an access link for the workflow ## Test releases @@ -32,6 +34,7 @@ and this project adheres to * [person-thirdparty]: fix quick-add of names that consist of multiple parts (eg. De Vlieger) within onthefly modal person/thirdparty * [search]: Order of birthdate fields changed in advanced search to avoid confusion. * [workflow]: Constraint added to workflow (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/675) +* [social_action]: only show active objectives (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/625) * [household]: Reposition and cut button for enfant hors menage have been deleted (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/620) * [admin]: Add crud for composition type in admin (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/611) diff --git a/phpstan-deprecations.neon b/phpstan-deprecations.neon index 1f6ca4401..9a6d862b5 100644 --- a/phpstan-deprecations.neon +++ b/phpstan-deprecations.neon @@ -175,13 +175,6 @@ parameters: count: 1 path: src/Bundle/ChillActivityBundle/Form/ActivityType.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php - message: """ @@ -294,14 +287,6 @@ parameters: count: 1 path: src/Bundle/ChillEventBundle/Form/Type/PickEventType.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillEventBundle/Search/EventSearch.php - - message: """ #^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\: @@ -390,13 +375,6 @@ parameters: count: 1 path: src/Bundle/ChillMainBundle/Export/ExportInterface.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillMainBundle/Export/ExportManager.php - message: """ @@ -754,14 +732,6 @@ parameters: count: 1 path: src/Bundle/ChillPersonBundle/Widget/PersonListWidget.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillReportBundle/Controller/ReportController.php - - message: """ #^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\: @@ -794,14 +764,6 @@ parameters: count: 1 path: src/Bundle/ChillReportBundle/Export/Filter/ReportDateFilter.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillReportBundle/Form/ReportType.php - - message: """ #^Parameter \\$role of method Chill\\\\ReportBundle\\\\Form\\\\ReportType\\:\\:appendScopeChoices\\(\\) has typehint with deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\: @@ -810,14 +772,6 @@ parameters: count: 1 path: src/Bundle/ChillReportBundle/Form/ReportType.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 1 - path: src/Bundle/ChillReportBundle/Search/ReportSearch.php - - message: """ #^Instantiation of deprecated class Symfony\\\\Component\\\\Security\\\\Core\\\\Role\\\\Role\\: @@ -826,13 +780,6 @@ parameters: count: 1 path: src/Bundle/ChillReportBundle/Search/ReportSearch.php - - - message: """ - #^Call to deprecated method getReachableScopes\\(\\) of class Chill\\\\MainBundle\\\\Security\\\\Authorization\\\\AuthorizationHelper\\: - Use getReachableCircles$# - """ - count: 2 - path: src/Bundle/ChillReportBundle/Timeline/TimelineReportProvider.php - message: """ diff --git a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php index 595eceb36..487d50e3a 100644 --- a/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php +++ b/src/Bundle/ChillCalendarBundle/Controller/CalendarController.php @@ -15,7 +15,7 @@ use Chill\CalendarBundle\Entity\Calendar; use Chill\CalendarBundle\Form\CalendarType; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; use Chill\CalendarBundle\Repository\CalendarACLAwareRepositoryInterface; -use Chill\CalendarBundle\Repository\CalendarRepository; +use Chill\DocGeneratorBundle\Repository\DocGeneratorTemplateRepository; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\UserRepository; @@ -42,7 +42,7 @@ class CalendarController extends AbstractController { private CalendarACLAwareRepositoryInterface $calendarACLAwareRepository; - private CalendarRepository $calendarRepository; + private DocGeneratorTemplateRepository $docGeneratorTemplateRepository; private FilterOrderHelperFactoryInterface $filterOrderHelperFactory; @@ -57,8 +57,8 @@ class CalendarController extends AbstractController private UserRepository $userRepository; public function __construct( - CalendarRepository $calendarRepository, CalendarACLAwareRepositoryInterface $calendarACLAwareRepository, + DocGeneratorTemplateRepository $docGeneratorTemplateRepository, FilterOrderHelperFactoryInterface $filterOrderHelperFactory, LoggerInterface $logger, PaginatorFactory $paginator, @@ -66,8 +66,8 @@ class CalendarController extends AbstractController SerializerInterface $serializer, UserRepository $userRepository ) { - $this->calendarRepository = $calendarRepository; $this->calendarACLAwareRepository = $calendarACLAwareRepository; + $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; $this->filterOrderHelperFactory = $filterOrderHelperFactory; $this->logger = $logger; $this->paginator = $paginator; @@ -152,7 +152,10 @@ class CalendarController extends AbstractController $view = '@ChillCalendar/Calendar/editByUser.html.twig'; } - $form = $this->createForm(CalendarType::class, $entity); + $form = $this->createForm(CalendarType::class, $entity) + ->add('save', SubmitType::class) + ->add('save_and_create_doc', SubmitType::class); + $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -162,6 +165,10 @@ class CalendarController extends AbstractController $params = $this->buildParamsToUrl($user, $accompanyingPeriod); + if ($form->get('save_and_create_doc')->isClicked()) { + return $this->redirectToRoute('chill_calendar_calendardoc_pick_template', ['id' => $entity->getId()]); + } + return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params); } @@ -214,6 +221,7 @@ class CalendarController extends AbstractController 'accompanyingCourse' => $accompanyingPeriod, 'paginator' => $paginator, 'filterOrder' => $filterOrder, + 'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class), ]); } @@ -276,7 +284,10 @@ class CalendarController extends AbstractController $entity->setAccompanyingPeriod($accompanyingPeriod); } - $form = $this->createForm(CalendarType::class, $entity); + $form = $this->createForm(CalendarType::class, $entity) + ->add('save', SubmitType::class) + ->add('save_and_create_doc', SubmitType::class); + $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -287,6 +298,10 @@ class CalendarController extends AbstractController $params = $this->buildParamsToUrl($user, $accompanyingPeriod); + if ($form->get('save_and_create_doc')->isClicked()) { + return $this->redirectToRoute('chill_calendar_calendardoc_pick_template', ['id' => $entity->getId()]); + } + return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params); } diff --git a/src/Bundle/ChillCalendarBundle/Controller/CalendarDocController.php b/src/Bundle/ChillCalendarBundle/Controller/CalendarDocController.php new file mode 100644 index 000000000..2c9074488 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Controller/CalendarDocController.php @@ -0,0 +1,82 @@ +security = $security; + $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; + $this->urlGenerator = $urlGenerator; + $this->engine = $engine; + } + + /** + * @Route("/{_locale}/calendar/docgen/pick/{id}", name="chill_calendar_calendardoc_pick_template") + */ + public function pickTemplate(Calendar $calendar): Response + { + if (!$this->security->isGranted(CalendarVoter::SEE, $calendar)) { + throw new AccessDeniedException('Not authorized to see this calendar'); + } + + if (0 === $number = $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) { + throw new RuntimeException('should not be redirected to this page if no template'); + } + + if (1 === $number) { + $templates = $this->docGeneratorTemplateRepository->findByEntity(Calendar::class); + + return new RedirectResponse( + $this->urlGenerator->generate( + 'chill_docgenerator_generate_from_template', + [ + 'template' => $templates[0]->getId(), + 'entityClassName' => Calendar::class, + 'entityId' => $calendar->getId(), + ] + ) + ); + } + + return new Response( + $this->engine->render('@ChillCalendar/CalendarDoc/pick_template.html.twig', [ + 'calendar' => $calendar, + 'accompanyingCourse' => $calendar->getAccompanyingPeriod(), + ]) + ); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index f23f07610..9104f6131 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -44,6 +44,9 @@ use function in_array; * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} * ) * @ORM\Entity + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ + * "chill_calendar_calendar": Calendar::class + * }) */ class Calendar implements TrackCreationInterface, TrackUpdateInterface { @@ -109,13 +112,24 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Embedded(class=CommentEmbeddable::class, columnPrefix="comment_") - * @Serializer\Groups({"calendar:read", "read"}) + * @Serializer\Groups({"calendar:read", "read", "docgen:read"}) */ private CommentEmbeddable $comment; + /** + * @ORM\Column(type="integer", nullable=false, options={"default": 0}) + */ + private int $dateTimeVersion = 0; + + /** + * @var Collection + * @ORM\OneToMany(targetEntity=CalendarDoc::class, mappedBy="calendar", orphanRemoval=true) + */ + private Collection $documents; + /** * @ORM\Column(type="datetime_immutable", nullable=false) - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) * @Assert\NotNull(message="calendar.An end date is required") */ private ?DateTimeImmutable $endDate = null; @@ -124,7 +138,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface * @ORM\Id * @ORM\GeneratedValue * @ORM\Column(type="integer") - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) */ private ?int $id = null; @@ -136,20 +150,20 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface * cascade={"persist", "remove", "merge", "detach"} * ) * @ORM\JoinTable(name="chill_calendar.calendar_to_invites") - * @Serializer\Groups({"read"}) + * @Serializer\Groups({"read", "docgen:read"}) */ private Collection $invites; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location") - * @Serializer\Groups({"read"}) + * @Serializer\Groups({"read", "docgen:read"}) * @Assert\NotNull(message="calendar.A location is required") */ private ?Location $location = null; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\NotNull(message="calendar.A main user is mandatory") */ @@ -158,7 +172,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\Person", inversedBy="calendars") * @ORM\JoinTable(name="chill_calendar.calendar_to_persons") - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\Count(min=1, minMessage="calendar.At least {{ limit }} person is required.") */ @@ -173,13 +187,14 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty") * @ORM\JoinTable(name="chill_calendar.calendar_to_thirdparties") - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) */ private Collection $professionals; /** * @ORM\Column(type="boolean", nullable=true) + * @Serializer\Groups({"docgen:read"}) */ private ?bool $sendSMS = false; @@ -190,7 +205,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Column(type="datetime_immutable", nullable=false) - * @Serializer\Groups({"calendar:read", "read", "calendar:light"}) + * @Serializer\Groups({"calendar:read", "read", "calendar:light", "docgen:read"}) * @Serializer\Context(normalizationContext={"read"}, groups={"calendar:light"}) * @Assert\NotNull(message="calendar.A start date is required") */ @@ -205,18 +220,32 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Column(type="boolean", nullable=true) + * @Serializer\Groups({"docgen:read"}) */ private ?bool $urgent = false; public function __construct() { $this->comment = new CommentEmbeddable(); + $this->documents = new ArrayCollection(); $this->privateComment = new PrivateCommentEmbeddable(); $this->persons = new ArrayCollection(); $this->professionals = new ArrayCollection(); $this->invites = new ArrayCollection(); } + /** + * @internal use @{CalendarDoc::__construct} instead + */ + public function addDocument(CalendarDoc $calendarDoc): self + { + if ($this->documents->contains($calendarDoc)) { + $this->documents[] = $calendarDoc; + } + + return $this; + } + /** * @internal Use {@link (Calendar::addUser)} instead */ @@ -282,6 +311,22 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this->comment; } + /** + * Each time the date and time is update, this version is incremented. + */ + public function getDateTimeVersion(): int + { + return $this->dateTimeVersion; + } + + public function getDocuments(): Collection + { + return $this->documents; + } + + /** + * @Serializer\Groups({"docgen:read"}) + */ public function getDuration(): ?DateInterval { if ($this->getStartDate() === null || $this->getEndDate() === null) { @@ -464,6 +509,18 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface ])); } + /** + * @internal use @{CalendarDoc::setCalendar} with null instead + */ + public function removeDocument(CalendarDoc $calendarDoc): self + { + if ($calendarDoc->getCalendar() !== $this) { + throw new LogicException('cannot remove document of another calendar'); + } + + return $this; + } + /** * @internal Use {@link (Calendar::removeUser)} instead */ @@ -554,6 +611,10 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface public function setEndDate(DateTimeImmutable $endDate): self { + if (null === $this->endDate || $this->endDate->getTimestamp() !== $endDate->getTimestamp()) { + $this->increaseaDatetimeVersion(); + } + $this->endDate = $endDate; return $this; @@ -601,6 +662,10 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface public function setStartDate(DateTimeImmutable $startDate): self { + if (null === $this->startDate || $this->startDate->getTimestamp() !== $startDate->getTimestamp()) { + $this->increaseaDatetimeVersion(); + } + $this->startDate = $startDate; return $this; @@ -623,4 +688,9 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this; } + + private function increaseaDatetimeVersion(): void + { + ++$this->dateTimeVersion; + } } diff --git a/src/Bundle/ChillCalendarBundle/Entity/CalendarDoc.php b/src/Bundle/ChillCalendarBundle/Entity/CalendarDoc.php new file mode 100644 index 000000000..458f38654 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Entity/CalendarDoc.php @@ -0,0 +1,135 @@ +setCalendar($calendar); + + $this->storedObject = $storedObject; + $this->datetimeVersion = $calendar->getDateTimeVersion(); + } + + public function getCalendar(): Calendar + { + return $this->calendar; + } + + public function getDatetimeVersion(): int + { + return $this->datetimeVersion; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getStoredObject(): StoredObject + { + return $this->storedObject; + } + + public function isTrackDateTimeVersion(): bool + { + return $this->trackDateTimeVersion; + } + + /** + * @internal use @see{Calendar::removeDocument} instead + * + * @param Calendar $calendar + */ + public function setCalendar(?Calendar $calendar): CalendarDoc + { + if (null === $calendar) { + $this->calendar->removeDocument($this); + } else { + $calendar->addDocument($this); + } + + $this->calendar = $calendar; + + $this->datetimeVersion = $calendar->getDateTimeVersion(); + + return $this; + } + + public function setDatetimeVersion(int $datetimeVersion): CalendarDoc + { + $this->datetimeVersion = $datetimeVersion; + + return $this; + } + + public function setStoredObject(StoredObject $storedObject): CalendarDoc + { + $this->storedObject = $storedObject; + + return $this; + } + + public function setTrackDateTimeVersion(bool $trackDateTimeVersion): CalendarDoc + { + $this->trackDateTimeVersion = $trackDateTimeVersion; + + return $this; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Entity/Invite.php b/src/Bundle/ChillCalendarBundle/Entity/Invite.php index 0489d0a00..c2d79aff2 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Invite.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Invite.php @@ -73,14 +73,14 @@ class Invite implements TrackUpdateInterface, TrackCreationInterface /** * @ORM\Column(type="text", nullable=false, options={"default": "pending"}) - * @Serializer\Groups(groups={"calendar:read", "read"}) + * @Serializer\Groups(groups={"calendar:read", "read", "docgen:read"}) */ private string $status = self::PENDING; /** * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") * @ORM\JoinColumn(nullable=false) - * @Serializer\Groups(groups={"calendar:read", "read"}) + * @Serializer\Groups(groups={"calendar:read", "read", "docgen:read"}) */ private ?User $user = null; diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php new file mode 100644 index 000000000..bd1074b5f --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepository.php @@ -0,0 +1,52 @@ +repository = $entityManager->getRepository($this->getClassName()); + } + + public function find($id): ?CalendarDoc + { + return $this->repository->find($id); + } + + public function findAll(): array + { + return $this->repository->findAll(); + } + + public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null) + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?CalendarDoc + { + return $this->findOneBy($criteria); + } + + public function getClassName() + { + return CalendarDoc::class; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php new file mode 100644 index 000000000..d2b1951df --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarDocRepositoryInterface.php @@ -0,0 +1,33 @@ + 0 %} + +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} +{% import "@ChillDocStore/Macro/macro_mimeicon.html.twig" as mm %} + + + +
+ + + + + + {% for d in calendar.documents %} + + + + {% endfor %} + +
+

{{ 'chill_calendar.Documents'|trans }}

+
+
    +
  • + {{ mm.mimeIcon(d.storedObject.type) }} + {{ d.storedObject.title }} + +
      + {% if chill_document_is_editable(d.storedObject) %} +
    • + {{ d.storedObject|chill_document_edit_button }} +
    • + {% endif %} +
    • + {{ m.download_button(d.storedObject, d.storedObject.title) }} +
    • +
    +
  • +
+
+
+{% endif %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig index f609b9d44..fce022ac4 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig @@ -77,11 +77,12 @@ {{ 'Cancel'|trans|chill_return_path_label }} -
  • - -
  • +
  • + {{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Save and add a document'|trans }) }} +
  • +
  • + {{ form_widget(form.save, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'Save'|trans }) }} +
  • {{ form_end(form) }} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig index e71c3f7d4..56d9a667d 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig @@ -1,4 +1,4 @@ -{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} {% set activeRouteKey = 'chill_calendar_calendar_list' %} @@ -10,186 +10,207 @@ {% block js %} {{ parent() }} {{ encore_entry_script_tags('mod_answer') }} + {{ encore_entry_script_tags('mod_async_upload') }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_answer') }} + {{ encore_entry_link_tags('mod_async_upload') }} {% endblock %} {% block content %} -

    {{ 'Calendar list' |trans }}

    +

    {{ 'Calendar list' |trans }}

    {{ filterOrder|chill_render_filter_order_helper }} {% if calendarItems|length == 0 %} -

    - {{ "There is no calendar items."|trans }} - -

    -{% else %} +

    + {{ "There is no calendar items."|trans }} + +

    + {% else %} -
    +
    - {% for calendar in calendarItems %} + {% for calendar in calendarItems %} -
    -
    -
    -
    -
    -
    - {% if calendar.endDate.diff(calendar.startDate).days >= 1 %} -

    {{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('short', 'short') }}

    - {% else %} -

    {{ calendar.startDate|format_datetime('short', 'short') }} - {{ calendar.endDate|format_datetime('none', 'short') }}

    - {% endif %} +
    +
    +
    +
    +
    +
    + {% if calendar.endDate.diff(calendar.startDate).days >= 1 %} +

    {{ calendar.startDate|format_datetime('short', 'short') }} + - {{ calendar.endDate|format_datetime('short', 'short') }}

    + {% else %} +

    {{ calendar.startDate|format_datetime('short', 'short') }} + - {{ calendar.endDate|format_datetime('none', 'short') }}

    + {% endif %} -
    -

    +

    - {{ calendar.duration|date('%H:%I')}} -

    -
    + {{ calendar.duration|date('%H:%I') }} + {% if false == calendar.sendSMS or null == calendar.sendSMS %} + + {% else %} + {% if calendar.smsStatus == 'sms_sent' %} + + + + + {% else %} + + + + + {% endif %} + {% endif %} +
    +
    -
    -
    -
      - {% if calendar.mainUser is not empty %} - {{ calendar.mainUser|chill_entity_render_box }} - {% endif %} -
    -
    +
    +
      + {% if calendar.mainUser is not empty %} + {{ calendar.mainUser|chill_entity_render_box }} + {% endif %} +
    +
    -
    -
    - - {% - if calendar.comment.comment is not empty - or calendar.users|length > 0 - or calendar.thirdParties|length > 0 - or calendar.users|length > 0 - %} -
    -
    - {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with { - 'context': 'calendar_accompanyingCourse', - 'render': 'wrap-list', - 'entity': calendar - } %} -
    - -
    - {% endif %} - - {% if calendar.comment.comment is not empty %} -
    -
    - {{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }}
    - {% endif %} - {% if calendar.location is not empty %} -
    + {% if calendar.comment.comment is not empty + or calendar.users|length > 0 + or calendar.thirdParties|length > 0 + or calendar.users|length > 0 %} +
    +
    + {% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with { + 'context': 'calendar_accompanyingCourse', + 'render': 'wrap-list', + 'entity': calendar + } %} +
    + +
    + {% endif %} + + {% if calendar.comment.comment is not empty %} +
    +
    + {{ calendar.comment|chill_entity_render_box( { 'limit_lines': 3, 'metadata': false } ) }} +
    +
    + {% endif %} + + {% if calendar.location is not empty %} +
    +
    + {% if calendar.location.address is not same as(null) and calendar.location.name is not empty %} + {% endif %} + {% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %} + {% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %} + {% endif %} + {% if calendar.location.phonenumber1 is not empty %} {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %} + {% if calendar.location.phonenumber2 is not empty %} {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %} +
    +
    + {% endif %} + +
    - {% if calendar.location.address is not same as(null) and calendar.location.name is not empty %}{% endif %} - {% if calendar.location.name is not empty %}{{ calendar.location.name }}{% endif %} - {% if calendar.location.address is not same as(null) %}{{ calendar.location.address|chill_entity_render_box({'multiline': false, 'with_picto': (calendar.location.name is empty)}) }}{% else %}{% endif %} - {% if calendar.location.phonenumber1 is not empty %} {{ calendar.location.phonenumber1|chill_format_phonenumber }}{% endif %} - {% if calendar.location.phonenumber2 is not empty %} {{ calendar.location.phonenumber2|chill_format_phonenumber }}{% endif %} + + {{ include('@ChillCalendar/Calendar/_documents.twig.html') }}
    - {% endif %} -
    -
    - {% if false == calendar.sendSMS or null == calendar.sendSMS %} - - - - - {% else %} - {% if calendar.smsStatus == 'sms_sent' %} - - - - - {% else %} - - - - +
    +
    - -
      - {% if is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) %} -
    • - - {{ 'Transform to activity'|trans }} - -
    • - {% endif %} - {% if (calendar.isInvited(app.user)) %} - {% set invite = calendar.inviteForUser(app.user) %} -
    • -
      -
    • - {% endif %} - {% if false %} -
    • - -
    • - {% endif %} - {# TOOD + {% if is_granted('CHILL_ACTIVITY_CREATE', accompanyingCourse) and calendar.activity is null %} +
    • + + {{ 'Transform to activity'|trans }} + +
    • + {% endif %} + {% if (calendar.isInvited(app.user)) %} + {% set invite = calendar.inviteForUser(app.user) %} +
    • +
      +
    • + {% endif %} + {% if false %} +
    • + +
    • + {% endif %} + {# TOOD {% if is_granted('CHILL_ACTIVITY_UPDATE', calendar) %} - #} -
    • - -
    • - {# TOOD + #} +
    • + +
    • + {# TOOD {% endif %} {% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %} - #} -
    • - -
    • - {# - {% endif %} - #} -
    + #} +
  • + +
  • + {# + {% endif %} + #} + + +
    + {% endfor %} -
    - {% endfor %} + {% if calendarItems|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} - {% if calendarItems|length < paginator.getTotalItems %} - {{ chill_pagination(paginator) }} +
    + {% endif %} + + +
    -{% endif %} - - - + {% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig index 26ce25655..2e2ca9ce1 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig @@ -78,9 +78,10 @@
  • - + {{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Create and add a document'|trans }) }} +
  • +
  • + {{ form_widget(form.save, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'Create'|trans }) }}
  • {{ form_end(form) }} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/CalendarDoc/pick_template.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/CalendarDoc/pick_template.html.twig new file mode 100644 index 000000000..561f92f0c --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/CalendarDoc/pick_template.html.twig @@ -0,0 +1,24 @@ +{% extends "@ChillPerson/AccompanyingCourse/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title %}{{ 'chill_calendar.Add a document' |trans }}{% endblock title %} + +{% set user_id = null %} +{% set accompanying_course_id = accompanyingCourse.id %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_docgen_picktemplate') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_docgen_picktemplate') }} +{% endblock %} + +{% block content %} + +
    + +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php new file mode 100644 index 000000000..9ba9e36b4 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContext.php @@ -0,0 +1,249 @@ +baseContextData = $baseContextData; + $this->entityManager = $entityManager; + $this->normalizer = $normalizer; + $this->personRender = $personRender; + $this->thirdPartyRender = $thirdPartyRender; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function adminFormReverseTransform(array $data): array + { + return array_merge( + [ + 'trackDatetime' => true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ], + $data + ); + } + + public function adminFormTransform(array $data): array + { + return $data; + } + + public function buildAdminForm(FormBuilderInterface $builder): void + { + $builder + ->add('trackDatetime', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Track changes on datetime and warn user if date time is updated after the doc generation', + ]) + ->add('askMainPerson', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Ask main person', + ]) + ->add('mainPersonLabel', TextType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Main person label', + ]) + ->add('askThirdParty', CheckboxType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Ask third party', + ]) + ->add('thirdPartyLabel', TextType::class, [ + 'required' => false, + 'label' => 'docgen.calendar.Third party label', + ]); + } + + public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void + { + $options = $this->getOptions($template); + + $builder->add('title', TextType::class, [ + 'required' => true, + 'label' => 'docgen.calendar.title of the generated document', + 'data' => $this->translatableStringHelper->localize($template->getName()), + ]); + + if ($options['askMainPerson']) { + $builder->add('mainPerson', EntityType::class, [ + 'class' => Person::class, + 'multiple' => false, + 'label' => $options['mainPersonLabel'] ?? 'docgen.calendar.Main person label', + 'required' => false, + 'choices' => $entity->getPersons(), + 'choice_label' => fn (Person $p) => $this->personRender->renderString($p, []), + 'expanded' => false, + ]); + } + + if ($options['askThirdParty']) { + $builder->add('thirdParty', EntityType::class, [ + 'class' => ThirdParty::class, + 'multiple' => false, + 'label' => $options['thirdPartyLabel'] ?? 'Third party', + 'choices' => $entity->getProfessionals(), + 'choice_label' => fn (ThirdParty $tp) => $this->thirdPartyRender->renderString($tp, []), + 'expanded' => false, + ]); + } + } + + /** + * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData + * @param mixed $entity + */ + public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array + { + $options = $this->getOptions($template); + + $data = array_merge( + $this->baseContextData->getData(), + [ + 'calendar' => $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Calendar::class, 'groups' => ['docgen:read']]), + ] + ); + + if ($options['askMainPerson']) { + $data['mainPerson'] = $this->normalizer->normalize($contextGenerationData['mainPerson'] ?? null, 'docgen', [ + 'docgen:expects' => Person::class, + 'groups' => ['docgen:read'], + 'docgen:person:with-household' => true, + 'docgen:person:with-relations' => true, + 'docgen:person:with-budget' => true, + ]); + } + + if ($options['askThirdParty']) { + $data['thirdParty'] = $this->normalizer->normalize($contextGenerationData['thirdParty'] ?? null, 'docgen', [ + 'docgen:expects' => ThirdParty::class, + 'groups' => ['docgen:read'], + ]); + } + + return $data; + } + + public function getDescription(): string + { + return 'docgen.calendar.A base context for generating document on calendar'; + } + + public function getEntityClass(): string + { + return Calendar::class; + } + + public function getFormData(DocGeneratorTemplate $template, $entity): array + { + $options = $this->getOptions($template); + $data = []; + + if ($options['askMainPerson']) { + $data['mainPerson'] = null; + + if (1 === count($entity->getPersons())) { + $data['mainPerson'] = $entity->getPersons()->first(); + } + } + + if ($options['askThirdParty']) { + $data['thirdParty'] = null; + + if (1 === count($entity->getProfessionals())) { + $data['thirdParty'] = $entity->getProfessionals()->first(); + } + } + + return $data; + } + + public static function getKey(): string + { + return self::class; + } + + public function getName(): string + { + return 'docgen.calendar.Base context for calendar'; + } + + public function hasAdminForm(): bool + { + return true; + } + + public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool + { + return true; + } + + /** + * @param array{mainPerson?: Person, thirdParty?: ThirdParty, title: string} $contextGenerationData + */ + public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void + { + $options = $this->getOptions($template); + $storedObject->setTitle($contextGenerationData['title']); + $doc = new CalendarDoc($entity, $storedObject); + $doc->setTrackDateTimeVersion($options['trackDatetime']); + + $this->entityManager->persist($doc); + } + + /** + * @return array{askMainPerson: bool, mainPersonLabel: ?string, askThirdParty: bool, thirdPartyLabel: ?string, trackDateTime: bool} $options + */ + private function getOptions(DocGeneratorTemplate $template): array + { + return $template->getOptions(); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php new file mode 100644 index 000000000..d02cdc2c2 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Service/DocGenerator/CalendarContextInterface.php @@ -0,0 +1,63 @@ +normalizer = self::$container->get(NormalizerInterface::class); + } + + public function testNormalizationCalendar() + { + $calendar = (new Calendar()) + ->setComment( + $comment = new CommentEmbeddable() + ) + ->setStartDate(DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, '2020-10-15T15:00:00+0000')) + ->setEndDate(DateTimeImmutable::createFromFormat(DateTimeImmutable::ATOM, '2020-15-15T15:30:00+0000')) + ->addPerson(new Person()) + ->addPerson(new Person()) + ->addUser(new User()) + ->addProfessional(new ThirdParty()); + + $expected = [ + 'type' => 'chill_calendar_calendar', + 'isNull' => false, + 'urgent' => false, + 'sendSMS' => false, + ]; + + $actual = $this->normalizer->normalize( + $calendar, + 'docgen', + ['groups' => ['docgen:read'], 'docgen:expects' => Calendar::class] + ); + + // we first check for the known key/value... + foreach ($expected as $key => $value) { + $this->assertArrayHasKey($key, $actual); + $this->assertEquals($value, $actual[$key]); + } + + // ... and then check for some other values + $this->assertArrayHasKey('persons', $actual); + $this->assertIsArray($actual['persons']); + $this->assertArrayHasKey('invites', $actual); + $this->assertIsArray($actual['invites']); + $this->assertArrayHasKey('startDate', $actual); + $this->assertIsArray($actual['startDate']); + $this->assertArrayHasKey('endDate', $actual); + $this->assertIsArray($actual['endDate']); + $this->assertArrayHasKey('professionals', $actual); + $this->assertIsArray($actual['professionals']); + $this->assertArrayHasKey('location', $actual); + $this->assertIsArray($actual['location']); + $this->assertArrayHasKey('mainUser', $actual); + $this->assertIsArray($actual['mainUser']); + $this->assertArrayHasKey('comment', $actual); + $this->assertIsArray($actual['comment']); + $this->assertArrayHasKey('duration', $actual); + $this->assertIsArray($actual['duration']); + } + + public function testNormalizationOnNullHasSameKeys() + { + $calendar = new Calendar(); + + $notNullCalendar = $this->normalizer->normalize( + $calendar, + 'docgen', + ['groups' => ['docgen:read'], 'docgen:expects' => Calendar::class] + ); + + $isNullCalendar = $this->normalizer->normalize( + null, + 'docgen', + ['groups' => ['docgen:read'], 'docgen:expects' => Calendar::class] + ); + + $this->assertEqualsCanonicalizing(array_keys($notNullCalendar), array_keys($isNullCalendar)); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php new file mode 100644 index 000000000..be31485d4 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/DocGenerator/CalendarContextTest.php @@ -0,0 +1,236 @@ + true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ]; + + $this->assertEqualsCanonicalizing($expected, $this->buildCalendarContext()->adminFormReverseTransform([])); + } + + public function testAdminFormTransform() + { + $expected = + [ + 'track_datetime' => true, + 'askMainPerson' => true, + 'mainPersonLabel' => 'docgen.calendar.Destinee', + 'askThirdParty' => false, + 'thirdPartyLabel' => 'Third party', + ]; + + $this->assertEqualsCanonicalizing($expected, $this->buildCalendarContext()->adminFormTransform($expected)); + } + + public function testBuildPublicForm() + { + $formBuilder = $this->prophesize(FormBuilderInterface::class); + $calendar = new Calendar(); + $calendar + ->addProfessional($tp1 = new ThirdParty()) + ->addProfessional($tp2 = new ThirdParty()) + ->addPerson($p1 = new Person()); + + // we will try once with askThirdParty = true, once with askPerson = true, and once with both + // so, we expect the call to be twice for each method + $formBuilder->add('thirdParty', EntityType::class, Argument::type('array')) + ->should(static function ($calls, $object, $method) use ($tp1, $tp2) { + if (2 !== count($calls)) { + throw new FailedPredictionException(sprintf('the $builder->add should be called exactly 2, %d receivved', count($calls))); + } + + $opts = $calls[0]->getArguments()[2]; + + if (!array_key_exists('label', $opts)) { + throw new FailedPredictionException('the $builder->add should have a label key'); + } + + if ('tplabel' !== $opts['label']) { + throw new FailedPredictionException('third party label not expected'); + } + + if (!$opts['choices']->contains($tp1) || !$opts['choices']->contains($tp2)) { + throw new FailedPredictionException('third party not present'); + } + }); + $formBuilder->add('mainPerson', EntityType::class, Argument::type('array')) + ->should(static function ($calls, $object, $method) use ($p1) { + if (2 !== count($calls)) { + throw new FailedPredictionException(sprintf('the $builder->add should be called exactly 2, %d receivved', count($calls))); + } + + $opts = $calls[0]->getArguments()[2]; + + if (!array_key_exists('label', $opts)) { + throw new FailedPredictionException('the $builder->add should have a label key'); + } + + if ('personLabel' !== $opts['label']) { + throw new FailedPredictionException('person label not expected'); + } + + if (!$opts['choices']->contains($p1)) { + throw new FailedPredictionException('person not present'); + } + }); + + $formBuilder->add('title', TextType::class, Argument::type('array')) + ->shouldBeCalledTimes(3); + + foreach ([ + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ['askMainPerson' => false, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => false, 'thirdPartyLabel' => 'tplabel'], + ] as $options) { + $template = new DocGeneratorTemplate(); + $template->setOptions($options); + + $this->buildCalendarContext()->buildPublicForm($formBuilder->reveal(), $template, $calendar); + } + } + + public function testGetData() + { + $calendar = (new Calendar()) + ->addPerson($p1 = new Person()) + ->addProfessional($t1 = new ThirdParty()); + $template = (new DocGeneratorTemplate())->setOptions( + ['askMainPerson' => true, 'mainPersonLabel' => 'personLabel', 'askThirdParty' => true, 'thirdPartyLabel' => 'tplabel'], + ); + $contextData = [ + 'mainPerson' => $p1, + 'thirdParty' => $t1, + ]; + + $normalizer = $this->prophesize(NormalizerInterface::class); + $normalizer->normalize($p1, 'docgen', Argument::type('array'))->willReturn(['person' => '1']); + $normalizer->normalize($t1, 'docgen', Argument::type('array'))->willReturn(['tp' => '1']); + $normalizer->normalize($calendar, 'docgen', Argument::type('array'))->willReturn(['calendar' => '1']); + + $actual = $this->buildCalendarContext(null, $normalizer->reveal()) + ->getData($template, $calendar, $contextData); + + $this->assertEqualsCanonicalizing([ + 'calendar' => ['calendar' => '1'], + 'mainPerson' => ['person' => '1'], + 'thirdParty' => ['tp' => '1'], + 'base_context' => 'data', + ], $actual); + } + + public function testStoreGenerated() + { + $calendar = new Calendar(); + $storedObject = new StoredObject(); + $contextData = ['title' => 'blabla']; + $template = (new DocGeneratorTemplate())->setOptions(['trackDatetime' => true]); + + $em = $this->prophesize(EntityManagerInterface::class); + $em->persist(Argument::type(CalendarDoc::class))->should( + static function ($calls, $object, $method) use ($storedObject) { + if (1 !== count($calls)) { + throw new FailedPredictionException('the persist method should be called once'); + } + + /** @var CalendarDoc $calendarDoc */ + $calendarDoc = $calls[0]->getArguments()[0]; + + if ($calendarDoc->getStoredObject() !== $storedObject) { + throw new FailedPredictionException('the stored object is not correct'); + } + + if ($calendarDoc->getStoredObject()->getTitle() !== 'blabla') { + throw new FailedPredictionException('the doc title should be the one provided'); + } + + if (!$calendarDoc->isTrackDateTimeVersion()) { + throw new FailedPredictionException('the track date time should be true'); + } + } + ); + + $this->buildCalendarContext($em->reveal())->storeGenerated($template, $storedObject, $calendar, $contextData); + } + + private function buildCalendarContext( + ?EntityManagerInterface $entityManager = null, + ?NormalizerInterface $normalizer = null + ): CalendarContext { + $baseContext = $this->prophesize(BaseContextData::class); + $baseContext->getData()->willReturn(['base_context' => 'data']); + + $personRender = $this->prophesize(PersonRender::class); + $personRender->renderString(Argument::type(Person::class), [])->willReturn('person name'); + + $thirdPartyRender = $this->prophesize(ThirdPartyRender::class); + $thirdPartyRender->renderString(Argument::type(ThirdParty::class), [])->willReturn('third party name'); + + $translatableStringHelper = $this->prophesize(TranslatableStringHelperInterface::class); + $translatableStringHelper->localize(Argument::type('array'))->willReturn('blabla'); + + if (null === $normalizer) { + $normalizer = $this->prophesize(NormalizerInterface::class)->reveal(); + } + + if (null === $entityManager) { + $entityManager = $this->prophesize(EntityManagerInterface::class)->reveal(); + } + + return new CalendarContext( + $baseContext->reveal(), + $entityManager, + $normalizer, + $personRender->reveal(), + $thirdPartyRender->reveal(), + $translatableStringHelper->reveal() + ); + } +} diff --git a/src/Bundle/ChillCalendarBundle/migrations/Version20221020101547.php b/src/Bundle/ChillCalendarBundle/migrations/Version20221020101547.php new file mode 100644 index 000000000..1fbfde60e --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/migrations/Version20221020101547.php @@ -0,0 +1,47 @@ +addSql('DROP SEQUENCE chill_calendar.calendar_doc_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_calendar.calendar_doc'); + $this->addSql('ALTER TABLE chill_calendar.calendar DROP dateTimeVersion'); + } + + public function getDescription(): string + { + return 'Add calendardoc on Calendar'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_calendar.calendar_doc_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_calendar.calendar_doc (id INT NOT NULL, calendar_id INT NOT NULL, datetimeVersion INT DEFAULT 0 NOT NULL, trackDateTimeVersion BOOLEAN DEFAULT false NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, storedObject_id INT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_4FD11573A40A2C8 ON chill_calendar.calendar_doc (calendar_id)'); + $this->addSql('CREATE INDEX IDX_4FD115736C99C13A ON chill_calendar.calendar_doc (storedObject_id)'); + $this->addSql('CREATE INDEX IDX_4FD115733174800F ON chill_calendar.calendar_doc (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_4FD1157365FF1AEC ON chill_calendar.calendar_doc (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_calendar.calendar_doc.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_calendar.calendar_doc.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_calendar.calendar_doc ADD CONSTRAINT FK_4FD11573A40A2C8 FOREIGN KEY (calendar_id) REFERENCES chill_calendar.calendar (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_calendar.calendar_doc ADD CONSTRAINT FK_4FD115736C99C13A FOREIGN KEY (storedObject_id) REFERENCES chill_doc.stored_object (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_calendar.calendar_doc ADD CONSTRAINT FK_4FD115733174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_calendar.calendar_doc ADD CONSTRAINT FK_4FD1157365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_calendar.calendar ADD dateTimeVersion INT DEFAULT 0 NOT NULL'); + } +} diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml index a03dd4015..bd9182e85 100644 --- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml @@ -50,6 +50,11 @@ chill_calendar: From: Du To: Au Next calendars: Prochains rendez-vous + Add a document: Ajouter un document + Documents: Documents + Create and add a document: Créer et ajouter un document + Save and add a document: Enregistrer et ajouter un document + remote_ms_graph: freebusy_statuses: @@ -112,3 +117,15 @@ has calendar range: Dans une plage de disponibilité? Not made within a calendar range: Rendez-vous dans une plage de disponibilité Made within a calendar range: Rendez-vous en dehors d'une plage de disponibilité +docgen: + calendar: + Base context for calendar: 'Rendez-vous: contexte de base' + A base context for generating document on calendar: Contexte pour générer des documents à partir des rendez-vous + Track changes on datetime and warn user if date time is updated after the doc generation: Suivre les changements sur le document et prévenir les utilisateurs que la date et l'heure ont été modifiée après la génération du document + Ask main person: Demander de choisir une personne parmi les participants aux rendez-vous + Main person label: Label pour choisir la personne + Ask third party: Demander de choisir un tiers parmi les participants aux rendez-vous + Third party label: Label pour choisir le tiers + Destinee: Destinataire + None: Aucun choix + title of the generated document: Titre du document généré diff --git a/src/Bundle/ChillDocGeneratorBundle/Controller/AdminDocGeneratorTemplateController.php b/src/Bundle/ChillDocGeneratorBundle/Controller/AdminDocGeneratorTemplateController.php index bb65fb5b2..c365b5e06 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Controller/AdminDocGeneratorTemplateController.php +++ b/src/Bundle/ChillDocGeneratorBundle/Controller/AdminDocGeneratorTemplateController.php @@ -14,6 +14,8 @@ namespace Chill\DocGeneratorBundle\Controller; use Chill\DocGeneratorBundle\Context\ContextManager; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\MainBundle\CRUD\Controller\CRUDController; +use Chill\MainBundle\Pagination\PaginatorInterface; +use Doctrine\ORM\QueryBuilder; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; @@ -84,4 +86,16 @@ class AdminDocGeneratorTemplateController extends CRUDController return $entity; } + + /** + * @param QueryBuilder $query + * + * @return QueryBuilder|mixed + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) + { + return $query->addSelect('JSON_EXTRACT(e.name, :lang) AS HIDDEN name_lang') + ->setParameter('lang', $request->getLocale()) + ->addOrderBy('name_lang', 'ASC'); + } } diff --git a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php index 4f800fd6a..c00dc7474 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php +++ b/src/Bundle/ChillDocGeneratorBundle/Repository/DocGeneratorTemplateRepository.php @@ -15,14 +15,18 @@ use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\Persistence\ObjectRepository; +use Symfony\Component\HttpFoundation\RequestStack; final class DocGeneratorTemplateRepository implements ObjectRepository { private EntityRepository $repository; - public function __construct(EntityManagerInterface $entityManager) + private RequestStack $requestStack; + + public function __construct(EntityManagerInterface $entityManager, RequestStack $requestStack) { $this->repository = $entityManager->getRepository(DocGeneratorTemplate::class); + $this->requestStack = $requestStack; } public function countByEntity(string $entity): int @@ -32,6 +36,7 @@ final class DocGeneratorTemplateRepository implements ObjectRepository $builder ->select('count(t)') ->where('t.entity LIKE :entity') + ->andWhere($builder->expr()->eq('t.active', "'TRUE'")) ->setParameter('entity', addslashes($entity)); return $builder->getQuery()->getSingleScalarResult(); @@ -71,7 +76,10 @@ final class DocGeneratorTemplateRepository implements ObjectRepository $builder ->where('t.entity LIKE :entity') ->andWhere($builder->expr()->eq('t.active', "'TRUE'")) - ->setParameter('entity', addslashes($entity)); + ->setParameter('entity', addslashes($entity)) + ->addSelect('JSON_EXTRACT(t.name, :lang) AS HIDDEN name_lang') + ->setParameter('lang', $this->requestStack->getCurrentRequest()->getLocale()) + ->addOrderBy('name_lang', 'ASC'); return $builder ->getQuery() diff --git a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/basic_form.html.twig b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/basic_form.html.twig index 7b24eae0d..35fa6e319 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/basic_form.html.twig +++ b/src/Bundle/ChillDocGeneratorBundle/Resources/views/Generator/basic_form.html.twig @@ -2,6 +2,14 @@ {% block title 'docgen.Generate a document'|trans %} +{% block js %} + {{ encore_entry_script_tags('mod_pickentity_type') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} + {% block content %}

    {{ block('title') }}

    diff --git a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php index de2a8775a..3250d4d5a 100644 --- a/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php +++ b/src/Bundle/ChillDocGeneratorBundle/tests/Serializer/Normalizer/DocGenObjectNormalizerTest.php @@ -77,6 +77,19 @@ final class DocGenObjectNormalizerTest extends KernelTestCase $this->assertArrayNotHasKey('baz', $actual['child']); } + public function testNormalizableBooleanPropertyOrMethodOnNull() + { + $actual = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableClassWithBool::class]); + + $expected = [ + 'foo' => null, + 'thing' => null, + 'isNull' => true, + ]; + + $this->assertEquals($expected, $actual); + } + public function testNormalizationBasic() { $scope = new Scope(); @@ -93,6 +106,22 @@ final class DocGenObjectNormalizerTest extends KernelTestCase $this->assertEquals($expected, $normalized, 'test normalization fo a scope'); } + public function testNormalizeBooleanPropertyOrMethod() + { + $testable = new TestableClassWithBool(); + $testable->foo = false; + + $actual = $this->normalizer->normalize($testable, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => TestableClassWithBool::class]); + + $expected = [ + 'foo' => false, + 'thing' => true, + 'isNull' => false, + ]; + + $this->assertEquals($expected, $actual); + } + public function testNormalizeNull() { $actual = $this->normalizer->normalize(null, 'docgen', [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Scope::class]); @@ -170,3 +199,19 @@ class TestableChildClass */ public string $foo = 'bar'; } + +class TestableClassWithBool +{ + /** + * @Serializer\Groups("docgen:read") + */ + public bool $foo; + + /** + * @Serializer\Groups("docgen:read") + */ + public function getThing(): bool + { + return true; + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php index 437f0930c..8e679b228 100644 --- a/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php +++ b/src/Bundle/ChillMainBundle/Controller/PostalCodeAPIController.php @@ -14,7 +14,7 @@ namespace Chill\MainBundle\Controller; use Chill\MainBundle\CRUD\Controller\ApiController; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\CountryRepository; -use Chill\MainBundle\Repository\PostalCodeRepository; +use Chill\MainBundle\Repository\PostalCodeRepositoryInterface; use Chill\MainBundle\Serializer\Model\Collection; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -30,11 +30,11 @@ final class PostalCodeAPIController extends ApiController private PaginatorFactory $paginatorFactory; - private PostalCodeRepository $postalCodeRepository; + private PostalCodeRepositoryInterface $postalCodeRepository; public function __construct( CountryRepository $countryRepository, - PostalCodeRepository $postalCodeRepository, + PostalCodeRepositoryInterface $postalCodeRepository, PaginatorFactory $paginatorFactory ) { $this->countryRepository = $countryRepository; diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 8e189e956..d7f3f0bc5 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -27,6 +27,7 @@ use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey; use Chill\MainBundle\Doctrine\DQL\JsonAggregate; use Chill\MainBundle\Doctrine\DQL\JsonbArrayLength; use Chill\MainBundle\Doctrine\DQL\JsonbExistsInArray; +use Chill\MainBundle\Doctrine\DQL\JsonExtract; use Chill\MainBundle\Doctrine\DQL\OverlapsI; use Chill\MainBundle\Doctrine\DQL\Replace; use Chill\MainBundle\Doctrine\DQL\Similarity; @@ -234,6 +235,7 @@ class ChillMainExtension extends Extension implements 'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class, 'AGGREGATE' => JsonAggregate::class, 'REPLACE' => Replace::class, + 'JSON_EXTRACT' => JsonExtract::class, ], 'numeric_functions' => [ 'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class, diff --git a/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php new file mode 100644 index 000000000..9f93c437e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/DQL/JsonExtract.php @@ -0,0 +1,43 @@ +>%s', $this->element->dispatch($sqlWalker), $this->keyToExtract->dispatch($sqlWalker)); + } + + public function parse(Parser $parser) + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->element = $parser->ArithmeticPrimary(); + + $parser->match(Lexer::T_COMMA); + + $this->keyToExtract = $parser->ArithmeticExpression(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/PostalCodeToIdTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/PostalCodeToIdTransformer.php new file mode 100644 index 000000000..ca488fe1a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/PostalCodeToIdTransformer.php @@ -0,0 +1,55 @@ +postalCodeRepository = $postalCodeRepository; + } + + public function reverseTransform($value) + { + if (null === $value || trim('') === $value) { + return null; + } + + if (!is_int((int) $value)) { + throw new TransformationFailedException('Cannot transform ' . gettype($value)); + } + + return $this->postalCodeRepository->find((int) $value); + } + + public function transform($value) + { + if (null === $value) { + return null; + } + + if ($value instanceof PostalCode) { + return $value->getId(); + } + + throw new TransformationFailedException('Could not reverseTransform ' . gettype($value)); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/PickPostalCodeType.php b/src/Bundle/ChillMainBundle/Form/Type/PickPostalCodeType.php new file mode 100644 index 000000000..d1feacd6a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/Type/PickPostalCodeType.php @@ -0,0 +1,49 @@ +postalCodeToIdTransformer = $postalCodeToIdTransformer; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addViewTransformer($this->postalCodeToIdTransformer); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['uniqid'] = $view->vars['attr']['data-input-postal-code'] = uniqid('input_pick_postal_code_'); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('class', PostalCode::class) + ->setDefault('multiple', false) + ->setAllowedTypes('multiple', ['bool']) + ->setDefault('compound', false); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php index c53df01df..32c1322be 100644 --- a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepository.php @@ -18,10 +18,9 @@ use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Query\ResultSetMappingBuilder; -use Doctrine\Persistence\ObjectRepository; use RuntimeException; -final class PostalCodeRepository implements ObjectRepository +final class PostalCodeRepository implements PostalCodeRepositoryInterface { private EntityManagerInterface $entityManager; @@ -29,7 +28,7 @@ final class PostalCodeRepository implements ObjectRepository public function __construct(EntityManagerInterface $entityManager) { - $this->repository = $entityManager->getRepository(PostalCode::class); + $this->repository = $entityManager->getRepository($this->getClassName()); $this->entityManager = $entityManager; } @@ -51,20 +50,11 @@ final class PostalCodeRepository implements ObjectRepository return $this->repository->find($id, $lockMode, $lockVersion); } - /** - * @return PostalCode[] - */ public function findAll(): array { return $this->repository->findAll(); } - /** - * @param mixed|null $limit - * @param mixed|null $offset - * - * @return PostalCode[] - */ public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array { return $this->repository->findBy($criteria, $orderBy, $limit, $offset); @@ -95,7 +85,7 @@ final class PostalCodeRepository implements ObjectRepository return $this->repository->findOneBy($criteria, $orderBy); } - public function getClassName() + public function getClassName(): string { return PostalCode::class; } diff --git a/src/Bundle/ChillMainBundle/Repository/PostalCodeRepositoryInterface.php b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepositoryInterface.php new file mode 100644 index 000000000..fe3dee195 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/PostalCodeRepositoryInterface.php @@ -0,0 +1,42 @@ +', + components: { + PickPostalCode, + }, + data() { + return { + city: city, + } + }, + methods: { + onCitySelected(city) { + this.city = city; + input.value = city.id; + }, + onCityRemoved(city) { + this.city = null; + input.value = ''; + } + } + }) + .use(i18n) + .mount(el); +} + +function loadDynamicPickers(element) { + + let apps = element.querySelectorAll('[data-module="pick-postal-code"]'); + + apps.forEach(function(el) { + + const + uniqId = el.dataset.uniqid, + input = document.querySelector(`input[data-input-uniqid="${uniqId}"]`), + cityIdValue = input.value === '' ? null : input.value + ; + + if (cityIdValue !== null) { + makeFetch('GET', `/api/1.0/main/postal-code/${cityIdValue}.json`).then(city => { + loadOnePicker(el, input, uniqId, city); + }) + } else { + loadOnePicker(el, input, uniqId, null); + } + }); +} + +document.addEventListener('DOMContentLoaded', function(e) { + loadDynamicPickers(document) +}) diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.md b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.md new file mode 100644 index 000000000..501e96983 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.md @@ -0,0 +1,29 @@ +# Pickpostalcode + +Allow to pick a postal code. + +In use with module `mod_pick_postal_code`, associated with `PickPostalCodeType` in php. + +## Usage + + `` + +## Props + +* `picked`: the city picked. A javascript object (a city). Null if empty. +* `country`: country to restraint search on picked. May be null. + +## Emits + +### `selectCity` + +When a city is onCitySelected. + +Argument: a js object, representing a city + +### `removeCity` + +When a city is removed. + + +Argument: a js object, representing a city diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.vue new file mode 100644 index 000000000..d5e55dbbd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/PickPostalCode.vue @@ -0,0 +1,107 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/_PickPostalCode.scss b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/_PickPostalCode.scss new file mode 100644 index 000000000..09f5fd539 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/_PickPostalCode.scss @@ -0,0 +1,3 @@ +.PickPostalCode { + +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/api.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/api.js new file mode 100644 index 000000000..d31dcc3f4 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/PickPostalCode/api.js @@ -0,0 +1,43 @@ +import {makeFetch, fetchResults} from 'ChillMainAssets/lib/api/apiMethods'; + +/** + * Endpoint chill_api_single_postal_code__index + * method GET, get Cities Object + * @params {object} a country object + * @returns {Promise} a promise containing all Postal Code objects filtered with country + */ +const fetchCities = (country) => { + // warning: do not use fetchResults (in apiMethods): we need only a **part** of the results in the db + const params = new URLSearchParams({item_per_page: 100}); + + if (country !== null) { + params.append('country', country.id); + } + + return makeFetch('GET', `/api/1.0/main/postal-code.json?${params.toString()}`).then(r => Promise.resolve(r.results)); +}; + +/** + * Endpoint chill_main_postalcodeapi_search + * method GET, get Cities Object + * @params {string} search a search string + * @params {object} country a country object + * @params {AbortController} an abort controller + * @returns {Promise} a promise containing all Postal Code objects filtered with country and a search string + */ +const searchCities = (search, country, controller) => { + const url = '/api/1.0/main/postal-code/search.json?'; + const params = new URLSearchParams({q: search}); + + if (country !== null) { + Object.assign('country', country.id); + } + + return makeFetch('GET', url + params, null, {signal: controller.signal}) + .then(result => Promise.resolve(result.results)); +}; + +export { + fetchCities, + searchCities, +}; diff --git a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig index 1710a91b8..2b006fc4d 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Form/fields.html.twig @@ -251,3 +251,9 @@
    {% endblock %} + +{% block pick_postal_code_widget %} + {{ form_help(form)}} + +
    +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig index bb0411371..bd9274739 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -95,6 +95,15 @@ {% endif %} + {% if entity_workflow.currentStep.destEmail|length > 0 %} +

    {{ 'workflow.An access key was also sent to those addresses'|trans }} :

    +
      + {% for e in entity_workflow.currentStep.destEmail -%} +
    • {{ e }}
    • + {%- endfor %} +
    + {% endif %} + {% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}

    {{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

      @@ -103,6 +112,21 @@ {% endfor %}
    {% endif %} + + {% if is_granted('CHILL_MAIN_WORKFLOW_LINK_SHOW', entity_workflow) %} +

    {{ 'workflow.This link grant any user to apply a transition'|trans }} :

    + + {% set link = absolute_url(path('chill_main_workflow_grant_access_by_key', {'id': entity_workflow.currentStep.id, 'accessKey': entity_workflow.currentStep.accessKey})) %} +
    + + + + +
    + {% endif %} + + {% endif %}
    diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig index dda309da0..72aab397c 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -81,6 +81,15 @@ {% endif %} + {% if entity_workflow.currentStep.destEmail|length > 0 %} +

    {{ 'workflow.An access key was also sent to those addresses'|trans }} :

    +
      + {% for e in entity_workflow.currentStep.destEmail -%} +
    • {{ e }}
    • + {%- endfor %} +
    + {% endif %} + {% if step.destUserByAccessKey|length > 0 %}

    {{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :

      diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php index f897bdb64..930a2c4d1 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/AuthorizationHelper.php @@ -190,8 +190,6 @@ class AuthorizationHelper implements AuthorizationHelperInterface /** * Return all reachable scope for a given user, center and role. * - * @deprecated Use getReachableCircles - * * @param Center|Center[] $center * * @return array|Scope[] diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php index 40dc1b13c..ffcc1bb95 100644 --- a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php +++ b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php @@ -27,6 +27,8 @@ class EntityWorkflowVoter extends Voter public const SEE = 'CHILL_MAIN_WORKFLOW_SEE'; + public const SHOW_ENTITY_LINK = 'CHILL_MAIN_WORKFLOW_LINK_SHOW'; + private EntityWorkflowManager $manager; private Security $security; @@ -80,6 +82,19 @@ class EntityWorkflowVoter extends Voter case self::DELETE: return $subject->getStep() === 'initial'; + case self::SHOW_ENTITY_LINK: + if ($subject->getStep() === 'initial') { + return false; + } + + $currentStep = $subject->getCurrentStepChained(); + + if ($currentStep->isFinal()) { + return false; + } + + return $currentStep->getPrevious()->getTransitionBy() === $this->security->getUser(); + default: throw new UnexpectedValueException("attribute {$attribute} not supported"); } @@ -91,6 +106,7 @@ class EntityWorkflowVoter extends Voter self::SEE, self::CREATE, self::DELETE, + self::SHOW_ENTITY_LINK, ]; } } diff --git a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php index 412a1269b..8dfc84bc3 100644 --- a/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php +++ b/src/Bundle/ChillMainBundle/Service/Import/AddressReferenceBEFromBestAddress.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\Service\Import; +use Exception; use League\Csv\Reader; use League\Csv\Statement; use RuntimeException; diff --git a/src/Bundle/ChillMainBundle/Test/ProphecyTrait.php b/src/Bundle/ChillMainBundle/Test/ProphecyTrait.php index 7e602a7dc..57768bf6e 100644 --- a/src/Bundle/ChillMainBundle/Test/ProphecyTrait.php +++ b/src/Bundle/ChillMainBundle/Test/ProphecyTrait.php @@ -17,6 +17,7 @@ namespace Chill\MainBundle\Test; * **Usage : ** You must set up trait with `setUpTrait` before use * and use tearDownTrait after usage. * + * @deprecated use @see{\Prophecy\PhpUnit\ProphecyTrait} instead * @codeCoverageIgnore * * @deprecated use @class{Prophecy\PhpUnit\ProphecyTrait} instead diff --git a/src/Bundle/ChillMainBundle/Tests/Doctrine/DQL/JsonExtractTest.php b/src/Bundle/ChillMainBundle/Tests/Doctrine/DQL/JsonExtractTest.php new file mode 100644 index 000000000..3f2b3eb2f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Doctrine/DQL/JsonExtractTest.php @@ -0,0 +1,52 @@ +em = self::$container->get(EntityManagerInterface::class); + } + + public function dataGenerateDql(): iterable + { + yield ['SELECT JSON_EXTRACT(c.name, \'fr\') FROM ' . Country::class . ' c', []]; + + yield ['SELECT JSON_EXTRACT(c.name, :lang) FROM ' . Country::class . ' c', ['lang' => 'fr']]; + } + + /** + * @dataProvider dataGenerateDql + */ + public function testJsonExtract(string $dql, array $args) + { + $results = $this->em->createQuery($dql) + ->setMaxResults(2) + ->setParameters($args) + ->getResult(); + + $this->assertIsArray($results, 'simply test that the query return a result'); + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Form/Type/PickPostalCodeTypeTest.php b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickPostalCodeTypeTest.php new file mode 100644 index 000000000..126130275 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Form/Type/PickPostalCodeTypeTest.php @@ -0,0 +1,70 @@ +factory->createBuilder(FormType::class, ['postal_code' => null]); + $builder->add('postal_code', PickPostalCodeType::class); + $form = $builder->getForm(); + + $form->submit(['postal_code' => '1']); + + $this->assertTrue($form->isSynchronized()); + + $this->assertEquals(1, $form['postal_code']->getData()->getId()); + } + + protected function getExtensions() + { + $postalCodeRepository = $this->prophesize(PostalCodeRepositoryInterface::class); + $postalCodeRepository->find(Argument::any()) + ->will(static function ($args) { + $postalCode = new PostalCode(); + $reflectionClass = new ReflectionClass($postalCode); + $id = $reflectionClass->getProperty('id'); + $id->setAccessible(true); + $id->setValue($postalCode, (int) $args[0]); + + return $postalCode; + }); + + $type = new PickPostalCodeType( + new PostalCodeToIdTransformer( + $postalCodeRepository->reveal() + ) + ); + + return [ + new PreloadedExtension([$type], []), + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php b/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php index 905ca1185..9305cbf09 100644 --- a/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php +++ b/src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php @@ -72,7 +72,7 @@ final class ValidPhonenumber extends ConstraintValidator } if (false === $isValid) { - $this->context->addViolation($message, ['%phonenumber%' => $value]); + $this->context->addViolation($message, ['%phonenumber%' => $value, '%formatted%' => $this->phonenumberHelper->format($value)]); } } } diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 628f04ba5..73f539739 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -70,6 +70,7 @@ module.exports = function(encore, entries) encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js'); encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js'); encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js'); + encore.addEntry('mod_pick_postal_code', __dirname + '/Resources/public/module/pick-postal-code/index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 1e95e32d0..36f793817 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -454,7 +454,6 @@ workflow: Delete workflow: Supprimer le workflow Steps is not waiting for transition. Maybe someone apply the transition before you ?: L'étape que vous cherchez a déjà été modifiée par un autre utilisateur. Peut-être quelqu'un a-t-il modifié cette étape avant vous ? You get access to this step: Vous avez acquis les droits pour appliquer une transition sur ce workflow. - Those users are also granted to apply a transition by using an access key: Ces utilisateurs peuvent également valider cette étape, grâce à un lien d'accès dest by email: Liens d'autorisation par email dest by email help: Les adresses email mentionnées ici recevront un lien d'accès. Ce lien d'accès permettra à l'utilisateur de valider cette étape. Add an email: Ajouter une adresse email @@ -466,6 +465,11 @@ workflow: Previous workflow transitionned help: Workflows où vous avez exécuté une action. For: Pour You must select a next step, pick another decision if no next steps are available: Il faut une prochaine étape. Choissisez une autre décision si nécessaire. + An access key was also sent to those addresses: Un lien d'accès a été envoyé à ces addresses + Those users are also granted to apply a transition by using an access key: Ces utilisateurs ont obtennu l'accès grâce au lien reçu par email + Access link copied: Lien d'accès copié + This link grant any user to apply a transition: Le lien d'accès suivant permet d'appliquer une transition + The workflow may be accssed through this link: Une transition peut être appliquée sur ce workflow grâce au lien d'accès suivant Subscribe final: Recevoir une notification à l'étape finale diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php index 9ab72bb2c..8d7249928 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php @@ -62,6 +62,7 @@ class UserRefEventSubscriber implements EventSubscriberInterface && $period->getUser() !== $this->security->getUser() && null !== $period->getUser() && $period->getStep() !== AccompanyingPeriod::STEP_DRAFT + && !$period->isPreventUserIsChangedNotification() ) { $this->generateNotificationToUser($period); } diff --git a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php index 181677709..91b834891 100644 --- a/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php +++ b/src/Bundle/ChillPersonBundle/Controller/ReassignAccompanyingPeriodController.php @@ -11,7 +11,9 @@ declare(strict_types=1); namespace Chill\PersonBundle\Controller; +use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Form\Type\PickPostalCodeType; use Chill\MainBundle\Form\Type\PickUserDynamicType; use Chill\MainBundle\Pagination\PaginatorFactory; use Chill\MainBundle\Repository\UserRepository; @@ -92,12 +94,14 @@ class ReassignAccompanyingPeriodController extends AbstractController $form->handleRequest($request); $userFrom = $form['user']->getData(); + $postalCodes = $form['postal_code']->getData() instanceof PostalCode ? [$form['postal_code']->getData()] : []; $total = $this->accompanyingPeriodACLAwareRepository->countByUserOpenedAccompanyingPeriod($userFrom); $paginator = $this->paginatorFactory->create($total); $periods = $this->accompanyingPeriodACLAwareRepository - ->findByUserOpenedAccompanyingPeriod( + ->findByUserAndPostalCodesOpenedAccompanyingPeriod( $userFrom, + $postalCodes, ['openingDate' => 'ASC'], $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber() @@ -123,7 +127,7 @@ class ReassignAccompanyingPeriodController extends AbstractController $period = $this->courseRepository->find($periodId); if ($period->getUser() === $userFrom) { - $period->setUser($userTo); + $period->setUser($userTo, true); } } @@ -148,7 +152,9 @@ class ReassignAccompanyingPeriodController extends AbstractController { $data = [ 'user' => null, + 'postal_code' => null, ]; + $builder = $this->formFactory->createBuilder(FormType::class, $data, [ 'method' => 'get', 'csrf_protection' => false, ]); @@ -158,12 +164,17 @@ class ReassignAccompanyingPeriodController extends AbstractController 'label' => 'reassign.Current user', 'required' => false, 'help' => 'reassign.Choose a user and click on "Filter" to apply', + ]) + ->add('postal_code', PickPostalCodeType::class, [ + 'label' => 'reassign.Filter by postal code', + 'required' => false, + 'help' => 'reassign.Filter course which are located inside a postal code', ]); return $builder->getForm(); } - private function buildReassignForm(array $periodIds, ?User $userFrom): FormInterface + private function buildReassignForm(array $periodIds, ?User $userFrom = null): FormInterface { $defaultData = [ 'userFrom' => $userFrom, diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index c6573d0d3..61c0291db 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -270,6 +270,8 @@ class AccompanyingPeriod implements */ private ?Comment $pinnedComment = null; + private bool $preventUserIsChangedNotification = false; + /** * @ORM\Column(type="text") * @Groups({"read", "write"}) @@ -1077,6 +1079,11 @@ class AccompanyingPeriod implements return false; } + public function isPreventUserIsChangedNotification(): bool + { + return $this->preventUserIsChangedNotification; + } + public function isRequestorAnonymous(): bool { return $this->requestorAnonymous; @@ -1372,11 +1379,12 @@ class AccompanyingPeriod implements return $this; } - public function setUser(?User $user): self + public function setUser(?User $user, bool $preventNotification = false): self { if ($this->user !== $user) { $this->userPrevious = $this->user; $this->userIsChanged = true; + $this->preventUserIsChangedNotification = $preventNotification; foreach ($this->userHistories as $history) { if (null === $history->getEndDate()) { diff --git a/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php b/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php index 445503045..e23038715 100644 --- a/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php +++ b/src/Bundle/ChillPersonBundle/Entity/MaritalStatus.php @@ -35,6 +35,11 @@ class MaritalStatus */ private array $name; + public function __construct() + { + $this->id = substr(md5(uniqid()), 0, 7); + } + /** * Get id. */ diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php index 5c05fe683..3324a236f 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/Evaluation.php @@ -72,6 +72,9 @@ class Evaluation $this->socialActions = new ArrayCollection(); } + /** + * @internal do use @see{SocialAction::addEvaluation} + */ public function addSocialAction(SocialAction $socialAction): self { if (!$this->socialActions->contains($socialAction)) { @@ -111,6 +114,11 @@ class Evaluation return $this->url; } + /** + * @return $this + * + * @internal do use @see{SocialAction::removeEvaluation} + */ public function removeSocialAction(SocialAction $socialAction): self { if ($this->socialActions->contains($socialAction)) { diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php index cd63fb830..6dfed9f34 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php @@ -112,6 +112,7 @@ class SocialAction { if (!$this->evaluations->contains($evaluation)) { $this->evaluations[] = $evaluation; + $evaluation->addSocialAction($this); } return $this; @@ -310,6 +311,7 @@ class SocialAction public function removeEvaluation(Evaluation $evaluation): self { $this->evaluations->removeElement($evaluation); + $evaluation->removeSocialAction($this); return $this; } diff --git a/src/Bundle/ChillPersonBundle/Form/MaritalStatusType.php b/src/Bundle/ChillPersonBundle/Form/MaritalStatusType.php index e12032755..6d71158d0 100644 --- a/src/Bundle/ChillPersonBundle/Form/MaritalStatusType.php +++ b/src/Bundle/ChillPersonBundle/Form/MaritalStatusType.php @@ -14,7 +14,6 @@ namespace Chill\PersonBundle\Form; use Chill\MainBundle\Form\Type\TranslatableStringFormType; use Chill\PersonBundle\Entity\MaritalStatus; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -26,9 +25,6 @@ class MaritalStatusType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { $builder - ->add('id', TextType::class, [ - 'label' => 'Identifiant', - ]) ->add('name', TranslatableStringFormType::class, [ 'label' => 'Nom', ]); diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php index f5217bfa7..071f7b3eb 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepository.php @@ -11,7 +11,9 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; +use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Location; +use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -19,10 +21,14 @@ use Chill\MainBundle\Security\Authorization\AuthorizationHelper; use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; +use Chill\PersonBundle\Entity\Household\PersonHouseholdAddress; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; use DateTime; +use DateTimeImmutable; +use Doctrine\DBAL\Types\Types; +use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Security\Core\Security; use function count; @@ -49,7 +55,12 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC $this->centerResolverDispatcher = $centerResolverDispatcher; } - public function buildQueryOpenedAccompanyingCourseByUser(?User $user) + /** + * @param array|PostalCode[] + * + * @return QueryBuilder + */ + public function buildQueryOpenedAccompanyingCourseByUser(?User $user, array $postalCodes = []) { $qb = $this->accompanyingPeriodRepository->createQueryBuilder('ap'); @@ -65,6 +76,37 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC ->setParameter('now', new DateTime('now')) ->setParameter('draft', AccompanyingPeriod::STEP_DRAFT); + if ([] !== $postalCodes) { + $qb->join('ap.locationHistories', 'location_history') + ->leftJoin(PersonHouseholdAddress::class, 'person_address', Join::WITH, 'IDENTITY(location_history.personLocation) = IDENTITY(person_address.person)') + ->join( + Address::class, + 'address', + Join::WITH, + 'COALESCE(IDENTITY(location_history.addressLocation), IDENTITY(person_address.address)) = address.id' + ) + ->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('person_address'), + $qb->expr()->andX( + $qb->expr()->lte('person_address.validFrom', ':now'), + $qb->expr()->orX( + $qb->expr()->isNull('person_address.validTo'), + $qb->expr()->lt('person_address.validTo', ':now') + ) + ) + ) + ) + ->andWhere( + $qb->expr()->isNull('location_history.endDate') + ) + ->andWhere( + $qb->expr()->in('address.postcode', ':postal_codes') + ) + ->setParameter('now', new DateTimeImmutable('now'), Types::DATE_IMMUTABLE) + ->setParameter('postal_codes', $postalCodes); + } + return $qb; } @@ -77,6 +119,18 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb->getQuery()->getSingleScalarResult(); } + public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int + { + if (null === $user) { + return 0; + } + + return $this->buildQueryOpenedAccompanyingCourseByUser($user, $postalCodes) + ->select('COUNT(ap)') + ->getQuery() + ->getSingleScalarResult(); + } + public function countByUserOpenedAccompanyingPeriod(?User $user): int { if (null === $user) { @@ -158,6 +212,24 @@ final class AccompanyingPeriodACLAwareRepository implements AccompanyingPeriodAC return $qb->getQuery()->getResult(); } + public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array + { + if (null === $user) { + return []; + } + + $qb = $this->buildQueryOpenedAccompanyingCourseByUser($user); + + $qb->setFirstResult($offset) + ->setMaxResults($limit); + + foreach ($orderBy as $field => $direction) { + $qb->addOrderBy('ap.' . $field, $direction); + } + + return $qb->getQuery()->getResult(); + } + /** * @return array|AccompanyingPeriod[] */ diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php index 6dd44b290..0cca1a5f4 100644 --- a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriodACLAwareRepositoryInterface.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository; +use Chill\MainBundle\Entity\PostalCode; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\UserJob; @@ -25,6 +26,11 @@ interface AccompanyingPeriodACLAwareRepositoryInterface */ public function countByUnDispatched(array $jobs, array $services, array $administrativeLocations): int; + /** + * @param array|PostalCode[] $postalCodes + */ + public function countByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes): int; + public function countByUserOpenedAccompanyingPeriod(?User $user): int; public function findByPerson( @@ -43,5 +49,10 @@ interface AccompanyingPeriodACLAwareRepositoryInterface */ public function findByUnDispatched(array $jobs, array $services, array $administrativeLocations, ?int $limit = null, ?int $offset = null): array; + /** + * @param array|PostalCode[] $postalCodes + */ + public function findByUserAndPostalCodesOpenedAccompanyingPeriod(?User $user, array $postalCodes, array $orderBy = [], int $limit = 0, int $offset = 50): array; + public function findByUserOpenedAccompanyingPeriod(?User $user, array $orderBy = [], int $limit = 0, int $offset = 50): array; } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php index f65467387..f6212e608 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/GoalRepository.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\Goal; use Chill\PersonBundle\Entity\SocialWork\SocialAction; +use DateTime; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\QueryBuilder; @@ -73,6 +74,14 @@ final class GoalRepository implements ObjectRepository $qb = $this->buildQueryBySocialActionWithDescendants($action); $qb->select('g'); + $qb->andWhere( + $qb->expr()->orX( + $qb->expr()->isNull('g.desactivationDate'), + $qb->expr()->gt('g.desactivationDate', ':now') + ) + ) + ->setParameter('now', new DateTime('now')); + foreach ($orderBy as $sort => $order) { $qb->addOrderBy('g.' . $sort, $order); } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig index 88bdf80c3..8cb5ab59f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/dispatch_list.html.twig @@ -76,7 +76,7 @@
      {% for period in periods %} {% include '@ChillPerson/AccompanyingPeriod/_list_item.html.twig' with {'period': period, - 'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period) } %} + 'recordAction': m.period_actions(period), 'itemMeta': m.period_meta(period), 'show_address': true } %} {% endfor %}
      {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig index cfa616988..6c7fbf7c0 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig @@ -113,6 +113,16 @@
    {% endif %} + {% if show_address|default(false) and period.location is not null %} +
    +

    {{ 'Accompanying course location'|trans }}

    +
    +

    + {{ period.location|chill_entity_render_string }} +

    +
    +
    + {% endif %}
    {% endif %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/reassign_list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/reassign_list.html.twig index 63e25efeb..c1144512d 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/reassign_list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/reassign_list.html.twig @@ -5,11 +5,13 @@ {% block js %} {{ encore_entry_script_tags('mod_set_referrer') }} {{ encore_entry_script_tags('mod_pickentity_type') }} + {{ encore_entry_script_tags('mod_pick_postal_code') }} {% endblock %} {% block css %} {{ encore_entry_link_tags('mod_set_referrer') }} {{ encore_entry_link_tags('mod_pickentity_type') }} + {{ encore_entry_link_tags('mod_pick_postal_code') }} {% endblock %} {% macro period_meta(period) %} @@ -48,6 +50,8 @@ {{ form_start(form) }} {{ form_label(form.user ) }} {{ form_widget(form.user, {'attr': {'class': 'select2'}}) }} + {{ form_label(form.postal_code) }} + {{ form_widget(form.postal_code) }}