diff --git a/docs/source/installation/index.rst b/docs/source/installation/index.rst index 00f27ed69..01a8c4aee 100644 --- a/docs/source/installation/index.rst +++ b/docs/source/installation/index.rst @@ -265,7 +265,7 @@ Tests reside inside the installed bundles. You must `cd` into that directory, do **Note**: some bundle require the fixture to be executed. See the dedicated _how-tos_. -Exemple, for running test inside `main` bundle: +Exemple, for running unit test inside `main` bundle: .. code-block:: bash @@ -280,6 +280,30 @@ Exemple, for running test inside `main` bundle: # run tests bin/phpunit src/Bundle/path/to/your/test +Or for running tests to check code style and php conventions with csfixer and phpstan: + +.. code-block:: bash + + # run code style fixer + bin/grumphp run --tasks=phpcsfixer + # run phpstan + bin/grumphp run --tasks=phpstan + + +.. note:: + + To avoid phpstan block your commits: + + .. code-block:: bash + + git commit -n ... + + To avoid phpstan block your commits permanently: + + .. code-block:: bash + + ./bin/grumphp git:deinit + How to run webpack interactively ================================ diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 15870060a..11db2dcb3 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -42,7 +42,6 @@ use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Role\Role; - use Symfony\Component\Serializer\SerializerInterface; use function array_key_exists; diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php index ffabc5934..322393f32 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/ByCreatorFilter.php @@ -62,7 +62,7 @@ class ByCreatorFilter implements FilterInterface } return ['Filtered activity by creator: only %users%', [ - '%users%' => implode(', ou ', $users), + '%users%' => implode(', ', $users), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php index e3ce8b287..d0c1b0fc7 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialActionFilter.php @@ -14,10 +14,9 @@ namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Entity\SocialWork\SocialAction; +use Chill\PersonBundle\Form\Type\PickSocialActionType; use Chill\PersonBundle\Templating\Entity\SocialActionRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; use function in_array; @@ -37,22 +36,17 @@ class BySocialActionFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - if (!in_array('actsocialaction', $qb->getAllAliases(), true)) { $qb->join('activity.socialActions', 'actsocialaction'); } $clause = $qb->expr()->in('actsocialaction.id', ':socialactions'); - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter('socialactions', $data['accepted_socialactions']); + $qb->andWhere($clause) + ->setParameter( + 'socialactions', + SocialAction::getDescendantsWithThisForActions($data['accepted_socialactions']) + ); } public function applyOn(): string @@ -62,13 +56,8 @@ class BySocialActionFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_socialactions', EntityType::class, [ - 'class' => SocialAction::class, - 'choice_label' => function (SocialAction $sa) { - return $this->actionRender->renderString($sa, []); - }, + $builder->add('accepted_socialactions', PickSocialActionType::class, [ 'multiple' => true, - 'expanded' => true, ]); } @@ -76,12 +65,14 @@ class BySocialActionFilter implements FilterInterface { $actions = []; - foreach ($data['accepted_socialactions'] as $sa) { - $actions[] = $this->actionRender->renderString($sa, []); + foreach ($data['accepted_socialactions'] as $action) { + $actions[] = $this->actionRender->renderString($action, [ + 'show_and_children' => true, + ]); } return ['Filtered activity by linked socialaction: only %actions%', [ - '%actions%' => implode(', ou ', $actions), + '%actions%' => implode(', ', $actions), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php index f5d552011..bbb882a65 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/BySocialIssueFilter.php @@ -14,10 +14,9 @@ namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Export\FilterInterface; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; +use Chill\PersonBundle\Form\Type\PickSocialIssueType; use Chill\PersonBundle\Templating\Entity\SocialIssueRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; use function in_array; @@ -37,22 +36,17 @@ class BySocialIssueFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { - $where = $qb->getDQLPart('where'); - if (!in_array('actsocialissue', $qb->getAllAliases(), true)) { $qb->join('activity.socialIssues', 'actsocialissue'); } $clause = $qb->expr()->in('actsocialissue.id', ':socialissues'); - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter('socialissues', $data['accepted_socialissues']); + $qb->andWhere($clause) + ->setParameter( + 'socialissues', + SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues']) + ); } public function applyOn(): string @@ -62,13 +56,8 @@ class BySocialIssueFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_socialissues', EntityType::class, [ - 'class' => SocialIssue::class, - 'choice_label' => function (SocialIssue $si) { - return $this->issueRender->renderString($si, []); - }, + $builder->add('accepted_socialissues', PickSocialIssueType::class, [ 'multiple' => true, - 'expanded' => true, ]); } @@ -76,12 +65,14 @@ class BySocialIssueFilter implements FilterInterface { $issues = []; - foreach ($data['accepted_socialissues'] as $si) { - $issues[] = $this->issueRender->renderString($si, []); + foreach ($data['accepted_socialissues'] as $issue) { + $issues[] = $this->issueRender->renderString($issue, [ + 'show_and_children' => true, + ]); } return ['Filtered activity by linked socialissue: only %issues%', [ - '%issues%' => implode(', ou ', $issues), + '%issues%' => implode(', ', $issues), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php index 023882cf9..5fe928b6c 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/LocationTypeFilter.php @@ -13,7 +13,7 @@ namespace Chill\ActivityBundle\Export\Filter\ACPFilters; use Chill\ActivityBundle\Export\Declarations; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\Select2LocationTypeType; +use Chill\MainBundle\Form\Type\PickLocationTypeType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; @@ -60,7 +60,7 @@ class LocationTypeFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_locationtype', Select2LocationTypeType::class, [ + $builder->add('accepted_locationtype', PickLocationTypeType::class, [ 'multiple' => true, //'label' => false, ]); @@ -77,7 +77,7 @@ class LocationTypeFilter implements FilterInterface } return ['Filtered activity by locationtype: only %types%', [ - '%types%' => implode(', ou ', $types), + '%types%' => implode(', ', $types), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php index 9f52c4694..6350f3ace 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserFilter.php @@ -71,7 +71,7 @@ class UserFilter implements FilterInterface } return ['Filtered activity by user: only %users%', [ - '%users%' => implode(', ou ', $users), + '%users%' => implode(', ', $users), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php index 4568cd006..1906db75e 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ACPFilters/UserScopeFilter.php @@ -85,7 +85,7 @@ class UserScopeFilter implements FilterInterface } return ['Filtered activity by userscope: only %scopes%', [ - '%scopes%' => implode(', ou ', $scopes), + '%scopes%' => implode(', ', $scopes), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php index 5338da791..d1758039a 100644 --- a/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php +++ b/src/Bundle/ChillActivityBundle/Export/Filter/ActivityTypeFilter.php @@ -91,7 +91,7 @@ class ActivityTypeFilter implements ExportElementValidatedInterface, FilterInter ); return ['Filtered by activity type: only %list%', [ - '%list%' => implode(', ou ', $reasonsNames), + '%list%' => implode(', ', $reasonsNames), ]]; } diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php new file mode 100644 index 000000000..dcdacd84a --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersJobFilter.php @@ -0,0 +1,81 @@ +translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . Activity::class . ' activity_users_job_filter_act + JOIN activity_users_job_filter_act.users users WHERE users.userJob IN (:activity_users_job_filter_jobs) AND activity_users_job_filter_act = activity ' + ) + ) + ->setParameter('activity_users_job_filter_jobs', $data['jobs']); + } + + public function applyOn() + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('jobs', EntityType::class, [ + 'class' => UserJob::class, + 'choice_label' => fn (UserJob $j) => $this->translatableStringHelper->localize($j->getLabel()), + 'multiple' => true, + 'expanded' => true, + ]); + } + + public function describeAction($data, $format = 'string') + { + return ['export.filter.activity.by_usersjob.Filtered activity by users job: only %jobs%', [ + '%jobs%' => implode( + ', ', + array_map( + fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()), + $data['jobs']->toArray() + ) + ), + ]]; + } + + public function getTitle() + { + return 'export.filter.activity.by_usersjob.Filter by users job'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php new file mode 100644 index 000000000..61b12264e --- /dev/null +++ b/src/Bundle/ChillActivityBundle/Export/Filter/UsersScopeFilter.php @@ -0,0 +1,88 @@ +scopeRepository = $scopeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb + ->andWhere( + $qb->expr()->exists( + 'SELECT 1 FROM ' . Activity::class . ' activity_users_scope_filter_act + JOIN activity_users_scope_filter_act.users users WHERE users.mainScope IN (:activity_users_scope_filter_scopes) AND activity_users_scope_filter_act = activity ' + ) + ) + ->setParameter('activity_users_scope_filter_scopes', $data['scopes']); + } + + public function applyOn() + { + return Declarations::ACTIVITY; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('scopes', EntityType::class, [ + 'class' => Scope::class, + 'choices' => $this->scopeRepository->findAllActive(), + 'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), + 'multiple' => true, + 'expanded' => true, + ]); + } + + public function describeAction($data, $format = 'string') + { + return ['export.filter.activity.by_usersscope.Filtered activity by users scope: only %scopes%', [ + '%scopes%' => implode( + ', ', + array_map( + fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), + $data['scopes']->toArray() + ) + ), + ]]; + } + + public function getTitle() + { + return 'export.filter.activity.by_usersscope.Filter by users scope'; + } +} diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig index 9b64403b1..96cfef39b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/concernedGroups.html.twig @@ -29,7 +29,7 @@ {% endmacro %} {% set blocks = [] %} -{% if context == 'calendar_accompanyingCourse' or entity.activityType.personsVisible %} +{% if context == 'calendar_accompanyingCourse' or context == 'calendar_person' or entity.activityType.personsVisible %} {% if context == 'person' %} {% set blocks = blocks|merge([{ 'title': 'Others persons'|trans, @@ -54,7 +54,7 @@ }]) %} {% endif %} {% endif %} -{% if context == 'calendar_accompanyingCourse' or entity.activityType.thirdPartiesVisible %} +{% if context == 'calendar_accompanyingCourse' or context == 'calendar_person' or entity.activityType.thirdPartiesVisible %} {% set blocks = blocks|merge([{ 'title': 'Third parties'|trans, 'items': entity.thirdParties, @@ -63,7 +63,7 @@ 'key' : 'id', }]) %} {% endif %} -{% if context == 'calendar_accompanyingCourse' or entity.activityType.usersVisible %} +{% if context == 'calendar_accompanyingCourse' or context == 'calendar_person' or entity.activityType.usersVisible %} {% set blocks = blocks|merge([{ 'title': 'Users concerned'|trans, 'items': entity.users, @@ -143,7 +143,7 @@ {% if bloc.type == 'user' %} {{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }} - {%- if context == 'calendar_accompanyingCourse' %} + {%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %} {% set invite = entity.inviteForUser(item) %} {% if invite is not null %} {{ invite.invite_span(invite) }} diff --git a/src/Bundle/ChillActivityBundle/config/services/export.yaml b/src/Bundle/ChillActivityBundle/config/services/export.yaml index 69565e29a..224075e6f 100644 --- a/src/Bundle/ChillActivityBundle/config/services/export.yaml +++ b/src/Bundle/ChillActivityBundle/config/services/export.yaml @@ -4,18 +4,15 @@ services: autoconfigure: true ## Indicators - chill.activity.export.count_activity_linked_to_person: - class: Chill\ActivityBundle\Export\Export\LinkedToPerson\CountActivity + Chill\ActivityBundle\Export\Export\LinkedToPerson\CountActivity: tags: - { name: chill.export, alias: 'count_activity_linked_to_person' } - chill.activity.export.sum_activity_duration_linked_to_person: - class: Chill\ActivityBundle\Export\Export\LinkedToPerson\StatActivityDuration + Chill\ActivityBundle\Export\Export\LinkedToPerson\StatActivityDuration: tags: - { name: chill.export, alias: 'sum_activity_duration_linked_to_person' } - chill.activity.export.list_activity_linked_to_person: - class: Chill\ActivityBundle\Export\Export\LinkedToPerson\ListActivity + Chill\ActivityBundle\Export\Export\LinkedToPerson\ListActivity: tags: - { name: chill.export, alias: 'list_activity_linked_to_person' } @@ -115,9 +112,16 @@ services: tags: - { name: chill.export_filter, alias: 'activity_userscope_filter' } + Chill\ActivityBundle\Export\Filter\UsersJobFilter: + tags: + - { name: chill.export_filter, alias: 'activity_usersjob_filter' } + + Chill\ActivityBundle\Export\Filter\UsersScopeFilter: + tags: + - { name: chill.export_filter, alias: 'activity_usersscope_filter' } + ## Aggregators - chill.activity.export.reason_aggregator: - class: Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator + Chill\ActivityBundle\Export\Aggregator\PersonAggregators\ActivityReasonAggregator: tags: - { name: chill.export_aggregator, alias: activity_reason_aggregator } diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml index 3a6ff1f05..6873b3736 100644 --- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml @@ -119,15 +119,15 @@ Activity Presences: Presences aux activités # Crud crud: - activity_type: - title_new: Nouveau type d'activité - title_edit: Edition d'un type d'activité - activity_type_category: - title_new: Nouvelle catégorie de type d'activité - title_edit: Edition d'une catégorie de type d'activité - activity_presence: - title_new: Nouvelle Présence aux activités - title_edit: Edition d'une Présence aux activités + activity_type: + title_new: Nouveau type d'activité + title_edit: Edition d'un type d'activité + activity_type_category: + title_new: Nouvelle catégorie de type d'activité + title_edit: Edition d'une catégorie de type d'activité + activity_presence: + title_new: Nouvelle Présence aux activités + title_edit: Edition d'une Présence aux activités # activity reason admin ActivityReason list: Liste des sujets @@ -322,3 +322,13 @@ docgen: A basic context for activity: Contexte pour les échanges Accompanying period with a list of activities: Parcours d'accompagnement avec liste des échanges Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les échanges pour un parcours. Les échanges ne sont pas filtrés. + +export: + filter: + activity: + by_usersjob: + Filter by users job: Filtrer les activités par métier d'au moins un utilisateur participant + 'Filtered activity by users job: only %jobs%': 'Filtré par métier d''au moins un utilisateur participant: seulement %jobs%' + by_usersscope: + Filter by users scope: Filtrer les activités par services d'au moins un utilisateur participant + 'Filtered activity by users scope: only %scopes%': 'Filtré par service d''au moins un utilisateur participant: seulement %scopes%' diff --git a/src/Bundle/ChillCalendarBundle/Command/AzureGrantAdminConsentAndAcquireToken.php b/src/Bundle/ChillCalendarBundle/Command/AzureGrantAdminConsentAndAcquireToken.php index 87a00d0b4..400619990 100644 --- a/src/Bundle/ChillCalendarBundle/Command/AzureGrantAdminConsentAndAcquireToken.php +++ b/src/Bundle/ChillCalendarBundle/Command/AzureGrantAdminConsentAndAcquireToken.php @@ -1,7 +1,7 @@ calendarRepository = $calendarRepository; $this->calendarACLAwareRepository = $calendarACLAwareRepository; + $this->docGeneratorTemplateRepository = $docGeneratorTemplateRepository; $this->filterOrderHelperFactory = $filterOrderHelperFactory; $this->logger = $logger; $this->paginator = $paginator; $this->remoteCalendarConnector = $remoteCalendarConnector; $this->serializer = $serializer; + $this->personRepository = $personRepository; + $this->accompanyingPeriodRepository = $accompanyingPeriodRepository; $this->userRepository = $userRepository; } @@ -83,19 +96,21 @@ class CalendarController extends AbstractController */ public function deleteAction(Request $request, Calendar $entity) { - $view = null; $em = $this->getDoctrine()->getManager(); - $accompanyingPeriod = $entity->getAccompanyingPeriod(); - $user = null; // TODO legacy code ? remove it ? + [$person, $accompanyingPeriod] = [$entity->getPerson(), $entity->getAccompanyingPeriod()]; if ($accompanyingPeriod instanceof AccompanyingPeriod) { $view = '@ChillCalendar/Calendar/confirm_deleteByAccompanyingCourse.html.twig'; - } elseif ($user instanceof User) { - $view = '@ChillCalendar/Calendar/confirm_deleteByUser.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]); + } elseif ($person instanceof Person) { + $view = '@ChillCalendar/Calendar/confirm_deleteByPerson.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]); + } else { + throw new RuntimeException('nor person or accompanying period'); } - $form = $this->createDeleteForm($entity->getId(), $user, $accompanyingPeriod); + $form = $this->createDeleteForm($entity); if ($request->getMethod() === Request::METHOD_DELETE) { $form->handleRequest($request); @@ -112,20 +127,15 @@ class CalendarController extends AbstractController $this->addFlash('success', $this->get('translator') ->trans('The calendar item has been successfully removed.')); - $params = $this->buildParamsToUrl($user, $accompanyingPeriod); - - return $this->redirectToRoute('chill_calendar_calendar_list_by_period', $params); + return new RedirectResponse($redirectRoute); } } - if (null === $view) { - throw $this->createNotFoundException('Template not found'); - } - return $this->render($view, [ 'calendar' => $entity, 'delete_form' => $form->createView(), 'accompanyingCourse' => $accompanyingPeriod, + 'person' => $person, ]); } @@ -140,19 +150,27 @@ class CalendarController extends AbstractController return $this->remoteCalendarConnector->getMakeReadyResponse($request->getUri()); } - $view = null; $em = $this->getDoctrine()->getManager(); - [$user, $accompanyingPeriod] = $this->getEntity($request); + [$person, $accompanyingPeriod] = [$entity->getPerson(), $entity->getAccompanyingPeriod()]; if ($accompanyingPeriod instanceof AccompanyingPeriod) { $view = '@ChillCalendar/Calendar/editByAccompanyingCourse.html.twig'; - } elseif ($user instanceof User) { - throw new Exception('to analyze'); - $view = '@ChillCalendar/Calendar/editByUser.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]); + } elseif ($person instanceof Person) { + $view = '@ChillCalendar/Calendar/editByPerson.html.twig'; + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]); + } else { + throw new RuntimeException('no person nor accompanying period'); + } + + $form = $this->createForm(CalendarType::class, $entity) + ->add('save', SubmitType::class); + + if (0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) { + $form->add('save_and_create_doc', SubmitType::class); } - $form = $this->createForm(CalendarType::class, $entity); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -160,29 +178,24 @@ class CalendarController extends AbstractController $this->addFlash('success', $this->get('translator')->trans('Success : calendar item updated!')); - $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); + return new RedirectResponse($redirectRoute); } if ($form->isSubmitted() && !$form->isValid()) { $this->addFlash('error', $this->get('translator')->trans('This form contains errors')); } - $deleteForm = $this->createDeleteForm($entity->getId(), $user, $accompanyingPeriod); - - if (null === $view) { - throw $this->createNotFoundException('Template not found'); - } - $entity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']); return $this->render($view, [ 'entity' => $entity, 'form' => $form->createView(), - 'delete_form' => $deleteForm->createView(), - 'accompanyingCourse' => $accompanyingPeriod, - // 'user' => $user, + 'accompanyingCourse' => $entity->getAccompanyingPeriod(), + 'person' => $entity->getPerson(), 'entity_json' => $entity_array, ]); } @@ -214,6 +227,38 @@ class CalendarController extends AbstractController 'accompanyingCourse' => $accompanyingPeriod, 'paginator' => $paginator, 'filterOrder' => $filterOrder, + 'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class), + ]); + } + + /** + * Lists all Calendar entities on a person. + * + * @Route("/{_locale}/calendar/calendar/by-person/{id}", name="chill_calendar_calendar_list_by_person") + */ + public function listActionByPerson(Person $person): Response + { + $filterOrder = $this->buildListFilterOrder(); + ['from' => $from, 'to' => $to] = $filterOrder->getDateRangeData('startDate'); + + $total = $this->calendarACLAwareRepository + ->countByPerson($person, $from, $to); + $paginator = $this->paginator->create($total); + $calendarItems = $this->calendarACLAwareRepository->findByPerson( + $person, + $from, + $to, + ['startDate' => 'DESC'], + $paginator->getCurrentPageFirstItemNumber(), + $paginator->getItemsPerPage() + ); + + return $this->render('@ChillCalendar/Calendar/listByPerson.html.twig', [ + 'calendarItems' => $calendarItems, + 'person' => $person, + 'paginator' => $paginator, + 'filterOrder' => $filterOrder, + 'hasDocs' => 0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class), ]); } @@ -253,30 +298,31 @@ class CalendarController extends AbstractController $view = null; $em = $this->getDoctrine()->getManager(); - [$user, $accompanyingPeriod] = $this->getEntity($request); + [$person, $accompanyingPeriod] = $this->getEntity($request); + + $entity = new Calendar(); if ($accompanyingPeriod instanceof AccompanyingPeriod) { $view = '@ChillCalendar/Calendar/newByAccompanyingCourse.html.twig'; + $entity->setAccompanyingPeriod($accompanyingPeriod); + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_period', ['id' => $accompanyingPeriod->getId()]); + } elseif ($person) { + $view = '@ChillCalendar/Calendar/newByPerson.html.twig'; + $entity->setPerson($person)->addPerson($person); + $redirectRoute = $this->generateUrl('chill_calendar_calendar_list_by_person', ['id' => $person->getId()]); } - // elseif ($user instanceof User) { - // $view = '@ChillCalendar/Calendar/newUser.html.twig'; - // } - - $entity = new Calendar(); if ($request->query->has('mainUser')) { $entity->setMainUser($this->userRepository->find($request->query->getInt('mainUser'))); } - // if ($user instanceof User) { - // $entity->setPerson($user); - // } + $form = $this->createForm(CalendarType::class, $entity) + ->add('save', SubmitType::class); - if ($accompanyingPeriod instanceof AccompanyingPeriod) { - $entity->setAccompanyingPeriod($accompanyingPeriod); + if (0 < $this->docGeneratorTemplateRepository->countByEntity(Calendar::class)) { + $form->add('save_and_create_doc', SubmitType::class); } - $form = $this->createForm(CalendarType::class, $entity); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { @@ -285,9 +331,11 @@ class CalendarController extends AbstractController $this->addFlash('success', $this->get('translator')->trans('Success : calendar item created!')); - $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); + return new RedirectResponse($redirectRoute); } if ($form->isSubmitted() && !$form->isValid()) { @@ -301,7 +349,8 @@ class CalendarController extends AbstractController $entity_array = $this->serializer->normalize($entity, 'json', ['groups' => 'read']); return $this->render($view, [ - 'user' => $user, + 'context' => $entity->getContext(), + 'person' => $person, 'accompanyingCourse' => $accompanyingPeriod, 'entity' => $entity, 'form' => $form->createView(), @@ -316,6 +365,7 @@ class CalendarController extends AbstractController */ public function showAction(Request $request, int $id): Response { + throw new Exception('not implemented'); $view = null; $em = $this->getDoctrine()->getManager(); @@ -450,49 +500,45 @@ class CalendarController extends AbstractController /** * Creates a form to delete a Calendar entity by id. */ - private function createDeleteForm(int $id, ?User $user, ?AccompanyingPeriod $accompanyingPeriod): FormInterface + private function createDeleteForm(Calendar $calendar): FormInterface { - $params = $this->buildParamsToUrl($user, $accompanyingPeriod); - $params['id'] = $id; - return $this->createFormBuilder() - ->setAction($this->generateUrl('chill_calendar_calendar_delete', $params)) + ->setAction($this->generateUrl('chill_calendar_calendar_delete', ['id' => $calendar->getId()])) ->setMethod('DELETE') ->add('submit', SubmitType::class, ['label' => 'Delete']) ->getForm(); } + /** + * @return array{0: ?Person, 1: ?AccompanyingPeriod} + */ private function getEntity(Request $request): array { $em = $this->getDoctrine()->getManager(); - $user = $accompanyingPeriod = null; + $person = $accompanyingPeriod = null; - if ($request->query->has('user_id')) { - $user_id = $request->get('user_id'); - $user = $em->getRepository(User::class)->find($user_id); + if ($request->query->has('person_id')) { + $person = $this->personRepository->find($request->query->getInt('person_id')); - if (null === $user) { - throw $this->createNotFoundException('User not found'); + if (null === $person) { + throw $this->createNotFoundException('Person not found'); } - // TODO Add permission - // $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $user); + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); } elseif ($request->query->has('accompanying_period_id')) { - $accompanying_period_id = $request->get('accompanying_period_id'); - $accompanyingPeriod = $em->getRepository(AccompanyingPeriod::class)->find($accompanying_period_id); + $accompanyingPeriod = $this->accompanyingPeriodRepository->find($request->query->getInt('accompanying_period_id')); if (null === $accompanyingPeriod) { throw $this->createNotFoundException('Accompanying Period not found'); } - // TODO Add permission - // $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person); + $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::SEE, $accompanyingPeriod); } else { throw $this->createNotFoundException('Person or Accompanying Period not found'); } return [ - $user, $accompanyingPeriod, + $person, $accompanyingPeriod, ]; } } 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/Controller/InviteApiController.php b/src/Bundle/ChillCalendarBundle/Controller/InviteApiController.php index 05ab52718..3016e1660 100644 --- a/src/Bundle/ChillCalendarBundle/Controller/InviteApiController.php +++ b/src/Bundle/ChillCalendarBundle/Controller/InviteApiController.php @@ -1,7 +1,7 @@ ''"})} * ) * @ORM\Entity + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ + * "chill_calendar_calendar": Calendar::class + * }) */ class Calendar implements TrackCreationInterface, TrackUpdateInterface { @@ -89,7 +92,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", inversedBy="calendars") */ - private AccompanyingPeriod $accompanyingPeriod; + private ?AccompanyingPeriod $accompanyingPeriod = null; /** * @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\Activity") @@ -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,29 +150,35 @@ 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") */ private ?User $mainUser = null; + /** + * @ORM\ManyToOne(targetEntity=Person::class) + * @ORM\JoinColumn(nullable=true) + */ + private ?Person $person = null; + /** * @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 +193,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 +211,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") */ @@ -203,15 +224,34 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface */ private string $status = self::STATUS_VALID; + /** + * @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 */ @@ -277,6 +317,38 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this->comment; } + /** + * @return 'person'|'accompanying_period'|null + */ + public function getContext(): ?string + { + if ($this->getAccompanyingPeriod() !== null) { + return 'accompanying_period'; + } + + if ($this->getPerson() !== null) { + return 'person'; + } + + return null; + } + + /** + * 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) { @@ -329,6 +401,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this->mainUser; } + public function getPerson(): ?Person + { + return $this->person; + } + /** * @return Collection|Person[] */ @@ -409,6 +486,11 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this->getProfessionals(); } + public function getUrgent(): ?bool + { + return $this->urgent; + } + /** * @return Collection|User[] * @Serializer\Groups({"calendar:read", "read"}) @@ -454,6 +536,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 */ @@ -544,6 +638,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; @@ -568,6 +666,13 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this; } + public function setPerson(?Person $person): Calendar + { + $this->person = $person; + + return $this; + } + public function setPrivateComment(PrivateCommentEmbeddable $privateComment): self { $this->privateComment = $privateComment; @@ -591,6 +696,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; @@ -606,4 +715,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface return $this; } + + public function setUrgent(bool $urgent): self + { + $this->urgent = $urgent; + + 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/Entity/RemoteCalendarTrait.php b/src/Bundle/ChillCalendarBundle/Entity/RemoteCalendarTrait.php index 9e162541d..31282fb0b 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/RemoteCalendarTrait.php +++ b/src/Bundle/ChillCalendarBundle/Entity/RemoteCalendarTrait.php @@ -1,7 +1,7 @@ getAllAliases(), true)) { - $qb->join('cal.user', 'caluser'); + $qb->join('cal.mainUser', 'caluser'); } $qb->addSelect('caluser.id AS agent_aggregator'); @@ -79,6 +79,6 @@ final class AgentAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by agent'; + return 'Group calendars by agent'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php index 53ecba909..329e2e50e 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/CancelReasonAggregator.php @@ -41,7 +41,7 @@ class CancelReasonAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { - // TODO: still needs to take into account appointments without a cancel reason somehow + // TODO: still needs to take into account calendars without a cancel reason somehow if (!in_array('calcancel', $qb->getAllAliases(), true)) { $qb->join('cal.cancelReason', 'calcancel'); } @@ -82,6 +82,6 @@ class CancelReasonAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by cancel reason'; + return 'Group calendars by cancel reason'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php index f56bc61cb..2a6f3b63e 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/JobAggregator.php @@ -42,7 +42,7 @@ final class JobAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { if (!in_array('caluser', $qb->getAllAliases(), true)) { - $qb->join('cal.user', 'caluser'); + $qb->join('cal.mainUser', 'caluser'); } $qb->addSelect('IDENTITY(caluser.userJob) as job_aggregator'); @@ -81,6 +81,6 @@ final class JobAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by agent job'; + return 'Group calendars by agent job'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php index cdd4c4b03..687dc9096 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationAggregator.php @@ -73,6 +73,6 @@ final class LocationAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by location'; + return 'Group calendars by location'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php index 6d42aed89..b23b304f6 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/LocationTypeAggregator.php @@ -81,6 +81,6 @@ final class LocationTypeAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by location type'; + return 'Group calendars by location type'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php index 11c6414ed..7b2a5e898 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/MonthYearAggregator.php @@ -59,6 +59,6 @@ class MonthYearAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by month and year'; + return 'Group calendars by month and year'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php index 11a71654e..b16b06d84 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/ScopeAggregator.php @@ -42,7 +42,7 @@ final class ScopeAggregator implements AggregatorInterface public function alterQuery(QueryBuilder $qb, $data) { if (!in_array('caluser', $qb->getAllAliases(), true)) { - $qb->join('cal.user', 'caluser'); + $qb->join('cal.mainUser', 'caluser'); } $qb->addSelect('IDENTITY(caluser.mainScope) as scope_aggregator'); @@ -81,6 +81,6 @@ final class ScopeAggregator implements AggregatorInterface public function getTitle(): string { - return 'Group appointments by agent scope'; + return 'Group calendars by agent scope'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php b/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php new file mode 100644 index 000000000..a80653441 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Export/Aggregator/UrgencyAggregator.php @@ -0,0 +1,96 @@ +translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->addSelect('cal.urgent AS urgency_aggregator'); + + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('urgency_aggregator'); + } else { + $qb->groupBy('urgency_aggregator'); + } + } + + public function applyOn(): string + { + return Declarations::CALENDAR_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + // no form + } + + public function getLabels($key, array $values, $data): Closure + { + return function ($value): string { + if ('_header' === $value) { + return 'Urgency'; + } + + switch ($value) { + case true: + return $this->translator->trans('is urgent'); + + case false: + return $this->translator->trans('is not urgent'); + + default: + throw new LogicException(sprintf('The value %s is not valid', $value)); + } + }; + } + + public function getQueryKeys($data): array + { + return ['urgency_aggregator']; + } + + public function getTitle(): string + { + return 'Group calendars by urgency'; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/CountAppointments.php b/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php similarity index 90% rename from src/Bundle/ChillCalendarBundle/Export/Export/CountAppointments.php rename to src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php index 9d659dc14..06d6defca 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/CountAppointments.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/CountCalendars.php @@ -21,10 +21,9 @@ use Closure; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\QueryBuilder; use Symfony\Component\Form\FormBuilderInterface; -use Symfony\Component\Process\Exception\LogicException; -use Symfony\Component\Security\Core\Role\Role; +use Symfony\Component\Validator\Exception\LogicException; -class CountAppointments implements ExportInterface, GroupedExportInterface +class CountCalendars implements ExportInterface, GroupedExportInterface { private CalendarRepository $calendarRepository; @@ -45,7 +44,7 @@ class CountAppointments implements ExportInterface, GroupedExportInterface public function getDescription(): string { - return 'Count appointments by various parameters.'; + return 'Count calendars by various parameters.'; } public function getGroup(): string @@ -79,7 +78,7 @@ class CountAppointments implements ExportInterface, GroupedExportInterface public function getTitle(): string { - return 'Count appointments'; + return 'Count calendars'; } public function getType(): string diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentAvgDuration.php b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php similarity index 92% rename from src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentAvgDuration.php rename to src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php index 50d8e2460..ddecba415 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentAvgDuration.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarAvgDuration.php @@ -22,7 +22,7 @@ use Doctrine\ORM\QueryBuilder; use LogicException; use Symfony\Component\Form\FormBuilderInterface; -class StatAppointmentAvgDuration implements ExportInterface, GroupedExportInterface +class StatCalendarAvgDuration implements ExportInterface, GroupedExportInterface { private CalendarRepository $calendarRepository; @@ -44,7 +44,7 @@ class StatAppointmentAvgDuration implements ExportInterface, GroupedExportInterf public function getDescription(): string { - return 'Get the average of appointment duration according to various filters'; + return 'Get the average of calendar duration according to various filters'; } public function getGroup(): string @@ -78,7 +78,7 @@ class StatAppointmentAvgDuration implements ExportInterface, GroupedExportInterf public function getTitle(): string { - return 'Average appointment duration'; + return 'Average calendar duration'; } public function getType(): string diff --git a/src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentSumDuration.php b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php similarity index 92% rename from src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentSumDuration.php rename to src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php index ff5fc8592..d99e73a2e 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Export/StatAppointmentSumDuration.php +++ b/src/Bundle/ChillCalendarBundle/Export/Export/StatCalendarSumDuration.php @@ -22,7 +22,7 @@ use Doctrine\ORM\QueryBuilder; use LogicException; use Symfony\Component\Form\FormBuilderInterface; -class StatAppointmentSumDuration implements ExportInterface, GroupedExportInterface +class StatCalendarSumDuration implements ExportInterface, GroupedExportInterface { private CalendarRepository $calendarRepository; @@ -44,7 +44,7 @@ class StatAppointmentSumDuration implements ExportInterface, GroupedExportInterf public function getDescription(): string { - return 'Get the sum of appointment durations according to various filters'; + return 'Get the sum of calendar durations according to various filters'; } public function getGroup(): string @@ -78,7 +78,7 @@ class StatAppointmentSumDuration implements ExportInterface, GroupedExportInterf public function getTitle(): string { - return 'Sum of appointment durations'; + return 'Sum of calendar durations'; } public function getType(): string diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php index 7c5d8e80e..18a4b0f4b 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/AgentFilter.php @@ -37,7 +37,7 @@ class AgentFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { $where = $qb->getDQLPart('where'); - $clause = $qb->expr()->in('cal.user', ':agents'); + $clause = $qb->expr()->in('cal.mainUser', ':agents'); if ($where instanceof Andx) { $where->add($clause); @@ -76,12 +76,12 @@ class AgentFilter implements FilterInterface return [ 'Filtered by agent: only %agents%', [ - '%agents' => implode(', ou ', $users), + '%agents' => implode(', ', $users), ], ]; } public function getTitle(): string { - return 'Filter appointments by agent'; + return 'Filter calendars by agent'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php index 65707d9e4..63004df21 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/BetweenDatesFilter.php @@ -66,7 +66,7 @@ class BetweenDatesFilter implements FilterInterface public function describeAction($data, $format = 'string'): array { - return ['Filtered by appointments between %dateFrom% and %dateTo%', [ + return ['Filtered by calendars between %dateFrom% and %dateTo%', [ '%dateFrom%' => $data['date_from']->format('d-m-Y'), '%dateTo%' => $data['date_to']->format('d-m-Y'), ]]; @@ -74,6 +74,6 @@ class BetweenDatesFilter implements FilterInterface public function getTitle(): string { - return 'Filter appointments between certain dates'; + return 'Filter calendars between certain dates'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php new file mode 100644 index 000000000..fde867b64 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/CalendarRangeFilter.php @@ -0,0 +1,109 @@ + true, + 'Made within a calendar range' => false, + ]; + + private const DEFAULT_CHOICE = false; + + private TranslatorInterface $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + + dump($data); + + if ($data['hasCalendarRange']) { + $clause = $qb->expr()->isNotNull('cal.calendarRange'); + } else { + $clause = $qb->expr()->isNull('cal.calendarRange'); + } + + if ($where instanceof Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + } + + public function applyOn(): string + { + return Declarations::CALENDAR_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('hasCalendarRange', ChoiceType::class, [ + 'choices' => self::CHOICES, + 'label' => 'has calendar range', + 'multiple' => false, + 'expanded' => true, + 'empty_data' => self::DEFAULT_CHOICE, + 'data' => self::DEFAULT_CHOICE, + ]); + } + + public function describeAction($data, $format = 'string'): array + { + foreach (self::CHOICES as $k => $v) { + if ($v === $data['hasCalendarRange']) { + $choice = $k; + } else { + $choice = 'Not made within a calendar range'; + } + } + + return [ + 'Filtered by calendar range: only %calendarRange%', [ + '%calendarRange%' => $this->translator->trans($choice), + ], + ]; + } + + public function getTitle(): string + { + return 'Filter by calendar range'; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php index f25ccad07..0f0f42adc 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/JobFilter.php @@ -44,7 +44,7 @@ class JobFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { if (!in_array('caluser', $qb->getAllAliases(), true)) { - $qb->join('cal.user', 'caluser'); + $qb->join('cal.mainUser', 'caluser'); } $where = $qb->getDQLPart('where'); @@ -90,12 +90,12 @@ class JobFilter implements FilterInterface } return ['Filtered by agent job: only %jobs%', [ - '%jobs%' => implode(', ou ', $userJobs), + '%jobs%' => implode(', ', $userJobs), ]]; } public function getTitle(): string { - return 'Filter appointments by agent job'; + return 'Filter calendars by agent job'; } } diff --git a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php index 15380c343..4d84543a3 100644 --- a/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php +++ b/src/Bundle/ChillCalendarBundle/Export/Filter/ScopeFilter.php @@ -44,7 +44,7 @@ class ScopeFilter implements FilterInterface public function alterQuery(QueryBuilder $qb, $data) { if (!in_array('caluser', $qb->getAllAliases(), true)) { - $qb->join('cal.user', 'caluser'); + $qb->join('cal.mainUser', 'caluser'); } $where = $qb->getDQLPart('where'); @@ -90,12 +90,12 @@ class ScopeFilter implements FilterInterface } return ['Filtered by agent scope: only %scopes%', [ - '%scopes%' => implode(', ou ', $scopes), + '%scopes%' => implode(', ', $scopes), ]]; } public function getTitle() { - return 'Filter appointments by agent scope'; + return 'Filter calendars by agent scope'; } } diff --git a/src/Bundle/ChillCalendarBundle/Form/DataTransformer/IdToCalendarRangeDataTransformer.php b/src/Bundle/ChillCalendarBundle/Form/DataTransformer/IdToCalendarRangeDataTransformer.php index 69bb9eca1..c9643b953 100644 --- a/src/Bundle/ChillCalendarBundle/Form/DataTransformer/IdToCalendarRangeDataTransformer.php +++ b/src/Bundle/ChillCalendarBundle/Form/DataTransformer/IdToCalendarRangeDataTransformer.php @@ -1,7 +1,7 @@ security = $security; + $this->translator = $translator; + } + + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + $person = $parameters['person']; + + if ($this->security->isGranted(CalendarVoter::SEE, $person)) { + $menu->addChild($this->translator->trans('Calendar'), [ + 'route' => 'chill_calendar_calendar_list_by_person', + 'routeParameters' => [ + 'id' => $person->getId(), + ], ]) + ->setExtras(['order' => 198]); + } + } + + public static function getMenuIds(): array + { + return ['person']; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php index db9276680..525039910 100644 --- a/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php +++ b/src/Bundle/ChillCalendarBundle/Menu/UserMenuBuilder.php @@ -12,51 +12,27 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Menu; use Chill\MainBundle\Routing\LocalMenuBuilderInterface; -use Chill\TaskBundle\Templating\UI\CountNotificationTask; use Knp\Menu\MenuItem; -use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Security; use Symfony\Contracts\Translation\TranslatorInterface; class UserMenuBuilder implements LocalMenuBuilderInterface { - /** - * @var AuthorizationCheckerInterface - */ - public $authorizationChecker; + public TranslatorInterface $translator; - /** - * @var CountNotificationTask - */ - public $counter; - - /** - * @var TokenStorageInterface - */ - public $tokenStorage; - - /** - * @var TranslatorInterface - */ - public $translator; + private Security $security; public function __construct( - CountNotificationTask $counter, - TokenStorageInterface $tokenStorage, - TranslatorInterface $translator, - AuthorizationCheckerInterface $authorizationChecker + Security $security, + TranslatorInterface $translator ) { - $this->counter = $counter; - $this->tokenStorage = $tokenStorage; + $this->security = $security; $this->translator = $translator; - $this->authorizationChecker = $authorizationChecker; } public function buildMenu($menuId, MenuItem $menu, array $parameters) { - $user = $this->tokenStorage->getToken()->getUser(); - - if ($this->authorizationChecker->isGranted('ROLE_USER')) { + if ($this->security->isGranted('ROLE_USER')) { $menu->addChild('My calendar list', [ 'route' => 'chill_calendar_calendar_list_my', ]) diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php b/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php index cbb765fba..94ba4974c 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Doctrine/CalendarEntityListener.php @@ -1,7 +1,7 @@ accompanyingPeriodACLAwareRepository = $accompanyingPeriodACLAwareRepository; $this->em = $em; } @@ -49,6 +64,46 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface return $qb; } + /** + * Base implementation. The list of allowed accompanying period is retrieved "manually" from @see{AccompanyingPeriodACLAwareRepository}. + */ + public function buildQueryByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): QueryBuilder + { + // find the reachable accompanying periods for person + $periods = $this->accompanyingPeriodACLAwareRepository->findByPerson($person, AccompanyingPeriodVoter::SEE); + + $qb = $this->em->createQueryBuilder() + ->from(Calendar::class, 'c'); + + $qb + ->where( + $qb->expr()->orX( + // the calendar where the person is the main person: + $qb->expr()->eq('c.person', ':person'), + // when the calendar is in a reachable period, and contains person + $qb->expr()->andX( + $qb->expr()->in('c.accompanyingPeriod', ':periods'), + $qb->expr()->isMemberOf(':person', 'c.persons') + ) + ) + ) + ->setParameter('person', $person) + ->setParameter('periods', $periods); + + // filter by date + if (null !== $startDate) { + $qb->andWhere($qb->expr()->gte('c.startDate', ':startDate')) + ->setParameter('startDate', $startDate); + } + + if (null !== $endDate) { + $qb->andWhere($qb->expr()->lte('c.endDate', ':endDate')) + ->setParameter('endDate', $endDate); + } + + return $qb; + } + public function countByAccompanyingPeriod(AccompanyingPeriod $period, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int { $qb = $this->buildQueryByAccompanyingPeriod($period, $startDate, $endDate)->select('count(c)'); @@ -56,6 +111,14 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface return $qb->getQuery()->getSingleScalarResult(); } + public function countByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate): int + { + return $this->buildQueryByPerson($person, $startDate, $endDate) + ->select('COUNT(c)') + ->getQuery() + ->getSingleScalarResult(); + } + /** * @return array|Calendar[] */ @@ -77,4 +140,24 @@ class CalendarACLAwareRepository implements CalendarACLAwareRepositoryInterface return $qb->getQuery()->getResult(); } + + public function findByPerson(Person $person, ?DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?array $orderBy = [], ?int $offset = null, ?int $limit = null): array + { + $qb = $this->buildQueryByPerson($person, $startDate, $endDate) + ->select('c'); + + foreach ($orderBy as $sort => $order) { + $qb->addOrderBy('c.' . $sort, $order); + } + + if (null !== $offset) { + $qb->setFirstResult($offset); + } + + if (null !== $limit) { + $qb->setMaxResults($limit); + } + + return $qb->getQuery()->getResult(); + } } diff --git a/src/Bundle/ChillCalendarBundle/Repository/CalendarACLAwareRepositoryInterface.php b/src/Bundle/ChillCalendarBundle/Repository/CalendarACLAwareRepositoryInterface.php index bc62a6a65..d860c3470 100644 --- a/src/Bundle/ChillCalendarBundle/Repository/CalendarACLAwareRepositoryInterface.php +++ b/src/Bundle/ChillCalendarBundle/Repository/CalendarACLAwareRepositoryInterface.php @@ -1,7 +1,7 @@ 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 @@ +getSingleScalarResult(); } + public function createQueryBuilder(string $alias, ?string $indexBy = null): QueryBuilder + { + return $this->repository->createQueryBuilder($alias, $indexBy); + } + public function find($id): ?Calendar { return $this->repository->find($id); diff --git a/src/Bundle/ChillCalendarBundle/Resources/config/services/exports.yaml b/src/Bundle/ChillCalendarBundle/Resources/config/services/exports.yaml index 0526ce1dc..3eb4bbfdc 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/config/services/exports.yaml +++ b/src/Bundle/ChillCalendarBundle/Resources/config/services/exports.yaml @@ -1,104 +1,118 @@ -#services: -# - # Indicators -# chill.calendar.export.count_appointments: -# class: Chill\CalendarBundle\Export\Export\CountAppointments -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export, alias: count_appointments } -# -# chill.calendar.export.average_duration_appointments: -# class: Chill\CalendarBundle\Export\Export\StatAppointmentAvgDuration -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export, alias: average_duration_appointments } -# -# chill.calendar.export.sum_duration_appointments: -# class: Chill\CalendarBundle\Export\Export\StatAppointmentSumDuration -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export, alias: sum_duration_appointments } -# - # Filters -# -# chill.calendar.export.agent_filter: -# class: Chill\CalendarBundle\Export\Filter\AgentFilter -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_filter, alias: agent_filter } -# -# chill.calendar.export.job_filter: -# class: Chill\CalendarBundle\Export\Filter\JobFilter -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_filter, alias: job_filter } -# -# chill.calendar.export.scope_filter: -# class: Chill\CalendarBundle\Export\Filter\ScopeFilter -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_filter, alias: scope_filter } -# -# chill.calendar.export.between_dates_filter: -# class: Chill\CalendarBundle\Export\Filter\BetweenDatesFilter -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_filter, alias: between_dates_filter } -# - # Aggregator -# -# chill.calendar.export.agent_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\AgentAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: agent_aggregator } -# -# chill.calendar.export.job_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\JobAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: job_aggregator } -# -# chill.calendar.export.scope_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\ScopeAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: scope_aggregator } -# -# chill.calendar.export.location_type_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\LocationTypeAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: location_type_aggregator } -# -# chill.calendar.export.location_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\LocationAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: location_aggregator } -# -# chill.calendar.export.cancel_reason_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\CancelReasonAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: cancel_reason_aggregator } -# -# chill.calendar.export.month_aggregator: -# class: Chill\CalendarBundle\Export\Aggregator\MonthYearAggregator -# autowire: true -# autoconfigure: true -# tags: -# - { name: chill.export_aggregator, alias: month_aggregator } +services: + + ## Indicators + chill.calendar.export.count_calendars: + class: Chill\CalendarBundle\Export\Export\CountCalendars + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: count_calendars } + + chill.calendar.export.average_duration_calendars: + class: Chill\CalendarBundle\Export\Export\StatCalendarAvgDuration + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: average_duration_calendars } + + chill.calendar.export.sum_duration_calendars: + class: Chill\CalendarBundle\Export\Export\StatCalendarSumDuration + autowire: true + autoconfigure: true + tags: + - { name: chill.export, alias: sum_duration_calendars } + + ## Filters + + chill.calendar.export.agent_filter: + class: Chill\CalendarBundle\Export\Filter\AgentFilter + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: agent_filter } + + chill.calendar.export.job_filter: + class: Chill\CalendarBundle\Export\Filter\JobFilter + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: job_filter } + + chill.calendar.export.scope_filter: + class: Chill\CalendarBundle\Export\Filter\ScopeFilter + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: scope_filter } + + chill.calendar.export.between_dates_filter: + class: Chill\CalendarBundle\Export\Filter\BetweenDatesFilter + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: between_dates_filter } + + chill.calendar.export.calendar_range_filter: + class: Chill\CalendarBundle\Export\Filter\CalendarRangeFilter + autowire: true + autoconfigure: true + tags: + - { name: chill.export_filter, alias: calendar_range_filter } + + ## Aggregator + + chill.calendar.export.agent_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\AgentAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: agent_aggregator } + + chill.calendar.export.job_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\JobAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: job_aggregator } + + chill.calendar.export.scope_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\ScopeAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: scope_aggregator } + + chill.calendar.export.location_type_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\LocationTypeAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: location_type_aggregator } + + chill.calendar.export.location_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\LocationAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: location_aggregator } + + chill.calendar.export.cancel_reason_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\CancelReasonAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: cancel_reason_aggregator } + + chill.calendar.export.month_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\MonthYearAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: month_aggregator } + + chill.calendar.export.urgency_aggregator: + class: Chill\CalendarBundle\Export\Aggregator\UrgencyAggregator + autowire: true + autoconfigure: true + tags: + - { name: chill.export_aggregator, alias: urgency_aggregator } diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html new file mode 100644 index 000000000..187f36dd7 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_documents.twig.html @@ -0,0 +1,45 @@ +{% if calendar.documents|length > 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/_list.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig new file mode 100644 index 000000000..3eb1d6634 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/_list.html.twig @@ -0,0 +1,177 @@ +{# list used in context of person or accompanyingPeriod #} + +{{ filterOrder|chill_render_filter_order_helper }} + +{% if calendarItems|length > 0 %} +
+ + {% for calendar in calendarItems %} + +
+
+
+
+
+
+

+ {% if context == 'person' and calendar.context == 'accompanying_period' %} + + + {{ calendar.accompanyingPeriod.id }} + + + {% 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') }} + {% 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.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.context == 'person' ? 'calendar_person' : '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 %} + +
+
+ + {{ include('@ChillCalendar/Calendar/_documents.twig.html') }} +
+
+ +
+
    + {% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', calendar) and hasDocs %} +
  • + + {{ 'chill_calendar.Add a document'|trans }} + +
  • + {% endif %} + {% if accompanyingCourse is defined and 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 + {% endif %} + {% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %} + #} +
  • + +
  • + {# + {% endif %} + #} +
+ +
+ +
+ {% endfor %} + + {% if calendarItems|length < paginator.getTotalItems %} + {{ chill_pagination(paginator) }} + {% endif %} + +
+{% endif %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/confirm_deleteByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/confirm_deleteByPerson.html.twig new file mode 100644 index 000000000..f72d0d21b --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/confirm_deleteByPerson.html.twig @@ -0,0 +1,16 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title 'Remove calendar item'|trans %} + +{% block content %} + {{ include('@ChillMain/Util/confirmation_template.html.twig', + { + 'title' : 'Remove calendar item'|trans, + 'confirm_question' : 'Are you sure you want to remove the calendar item?'|trans, + 'cancel_route' : 'chill_calendar_calendar_list_by_period', + 'cancel_parameters' : { 'id' : person.id }, + 'form' : delete_form + } ) }} +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig index f609b9d44..7d72922d1 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/edit.html.twig @@ -77,11 +77,14 @@ {{ 'Cancel'|trans|chill_return_path_label }} -
  • - -
  • + {% if form.save_and_create_doc is defined %} +
  • + {{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Save and add a document'|trans }) }} +
  • + {% endif %} +
  • + {{ form_widget(form.save, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'Save'|trans }) }} +
  • {{ form_end(form) }} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/editByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/editByPerson.html.twig new file mode 100644 index 000000000..fc5319849 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/editByPerson.html.twig @@ -0,0 +1,34 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title 'Update calendar'|trans %} + +{% block content %} +
    + + {% include 'ChillCalendarBundle:Calendar:edit.html.twig' with {'context': 'person'} %} + +
    +{% endblock %} + +{% block js %} + {{ parent() }} + + {{ encore_entry_script_tags('vue_calendar') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('vue_calendar') }} + {{ encore_entry_link_tags('page_calendar') }} +{% endblock %} + +{% block block_post_menu %} +
    +{% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByAccompanyingCourse.html.twig index e71c3f7d4..f1b0c6110 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,44 @@ {% 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 }}

    - {{ filterOrder|chill_render_filter_order_helper }} +

    {{ 'Calendar list' |trans }}

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

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

    -{% else %} +

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

    + {% else %} + {{ include('@ChillCalendar/Calendar/_list.html.twig', {context: 'accompanying_course'}) }} + {% endif %} -
    - - {% 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 %} - -
    -

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

    -
    - -
    -
    -
    - -
    -
      - {% 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.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 false == calendar.sendSMS or null == calendar.sendSMS %} - - - - - {% else %} - {% if calendar.smsStatus == 'sms_sent' %} - - - - - {% else %} - - - - - {% endif %} - {% endif %} -
    - -
      - {% 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_UPDATE', calendar) %} - #} -
    • - -
    • - {# TOOD - {% endif %} - {% if is_granted('CHILL_ACTIVITY_DELETE', calendar) %} - #} -
    • - -
    • - {# - {% endif %} - #} -
    - -
    - -
    - {% endfor %} - - {% if calendarItems|length < paginator.getTotalItems %} - {{ chill_pagination(paginator) }} +
    -{% endif %} - - - + {% endblock %} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig new file mode 100644 index 000000000..6d991fb53 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/listByPerson.html.twig @@ -0,0 +1,52 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_list' %} + +{% block title %}{{ 'Calendar list' |trans }}{% endblock title %} + +{% set user_id = null %} + +{% 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 }}

    + + {% if calendarItems|length == 0 %} +

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

    + {% else %} + {{ include ('@ChillCalendar/Calendar/_list.html.twig', {context: 'person'}) }} + {% 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..3e463bb05 100644 --- a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/new.html.twig @@ -69,7 +69,7 @@ + {% if form.save_and_create_doc is defined %} +
  • + {{ form_widget(form.save_and_create_doc, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'chill_calendar.Create and add a document'|trans }) }} +
  • + {% endif %}
  • - + {{ form_widget(form.save, { 'attr' : { 'class' : 'btn btn-create' }, 'label': 'Create'|trans }) }}
  • {{ form_end(form) }} diff --git a/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/newByPerson.html.twig b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/newByPerson.html.twig new file mode 100644 index 000000000..b561e6aa7 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Resources/views/Calendar/newByPerson.html.twig @@ -0,0 +1,34 @@ +{% extends "@ChillPerson/Person/layout.html.twig" %} + +{% set activeRouteKey = 'chill_calendar_calendar_new' %} + +{% block title 'Calendar item creation' |trans %} + +{% block content %} +
    + +
    {# <=== vue component #} + {% include 'ChillCalendarBundle:Calendar:new.html.twig' with {'context': 'person'} %} + +
    +{% endblock %} + +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_pickentity_type') }} + + {{ encore_entry_script_tags('vue_calendar') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('vue_calendar') }} + {{ encore_entry_link_tags('mod_pickentity_type') }} +{% endblock %} + +{% block block_post_menu %} +
    +{% endblock %} 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/Security/Voter/CalendarVoter.php b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php index 54bdcb5fb..ded8d2062 100644 --- a/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php +++ b/src/Bundle/ChillCalendarBundle/Security/Voter/CalendarVoter.php @@ -1,7 +1,7 @@ voterHelper = $voterHelperFactory ->generate(self::class) ->addCheckFor(AccompanyingPeriod::class, [self::SEE]) + ->addCheckFor(Person::class, [self::SEE]) ->addCheckFor(Calendar::class, [self::SEE, self::CREATE, self::EDIT, self::DELETE]) ->build(); } @@ -82,6 +92,14 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn // we first check here that the user has read access to the period return $this->security->isGranted(AccompanyingPeriodVoter::SEE, $subject); + default: + throw new LogicException('subject not implemented'); + } + } elseif ($subject instanceof Person) { + switch ($attribute) { + case self::SEE: + return $this->security->isGranted(PersonVoter::SEE, $subject); + default: throw new LogicException('subject not implemented'); } @@ -96,6 +114,16 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn case self::DELETE: return $this->security->isGranted(AccompanyingPeriodVoter::EDIT, $period); } + } elseif (null !== $person = $subject->getPerson()) { + switch ($attribute) { + case self::SEE: + case self::EDIT: + case self::CREATE: + return $this->security->isGranted(PersonVoter::SEE, $person); + + case self::DELETE: + return $this->security->isGranted(PersonVoter::UPDATE, $person); + } } } diff --git a/src/Bundle/ChillCalendarBundle/Security/Voter/InviteVoter.php b/src/Bundle/ChillCalendarBundle/Security/Voter/InviteVoter.php index 83caa5f64..36ca66d05 100644 --- a/src/Bundle/ChillCalendarBundle/Security/Voter/InviteVoter.php +++ b/src/Bundle/ChillCalendarBundle/Security/Voter/InviteVoter.php @@ -1,7 +1,7 @@ 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 @@ +aggregator = self::$container->get('chill.calendar.export.agent_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.mainUser', 'caluser'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/CancelReasonAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/CancelReasonAggregatorTest.php new file mode 100644 index 000000000..956a81174 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/CancelReasonAggregatorTest.php @@ -0,0 +1,68 @@ +aggregator = self::$container->get('chill.calendar.export.cancel_reason_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.cancelReason', 'calcancel'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/JobAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/JobAggregatorTest.php new file mode 100644 index 000000000..b389b1536 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/JobAggregatorTest.php @@ -0,0 +1,68 @@ +aggregator = self::$container->get('chill.calendar.export.job_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.user', 'caluser'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationAggregatorTest.php new file mode 100644 index 000000000..50ee456e5 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationAggregatorTest.php @@ -0,0 +1,68 @@ +aggregator = self::$container->get('chill.calendar.export.location_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.location', 'calloc'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationTypeAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationTypeAggregatorTest.php new file mode 100644 index 000000000..55dcf317f --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/LocationTypeAggregatorTest.php @@ -0,0 +1,68 @@ +aggregator = self::$container->get('chill.calendar.export.location_type_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.location', 'calloc'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/MonthYearAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/MonthYearAggregatorTest.php new file mode 100644 index 000000000..8e016b54c --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/MonthYearAggregatorTest.php @@ -0,0 +1,67 @@ +aggregator = self::$container->get('chill.calendar.export.month_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/ScopeAggregatorTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/ScopeAggregatorTest.php new file mode 100644 index 000000000..fa816f4a5 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Aggregator/ScopeAggregatorTest.php @@ -0,0 +1,68 @@ +aggregator = self::$container->get('chill.calendar.export.scope_aggregator'); + } + + public function getAggregator() + { + return $this->aggregator; + } + + public function getFormData(): array + { + return [ + [], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('count(cal.id)') + ->from(Calendar::class, 'cal') + ->join('cal.user', 'caluser'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/AgentFilterTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/AgentFilterTest.php new file mode 100644 index 000000000..d5ec9b22d --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/AgentFilterTest.php @@ -0,0 +1,88 @@ +prophesize(); + + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $this->filter = self::$container->get('chill.calendar.export.agent_filter'); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + $em = self::$container->get(EntityManagerInterface::class); + + $array = $em->createQueryBuilder() + ->from(User::class, 'u') + ->select('u') + ->getQuery() + ->getResult(); + + $data = []; + + foreach ($array as $a) { + $data[] = [ + 'accepted_agents' => $a, + ]; + } + + return $data; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('cal.id') + ->from(Calendar::class, 'cal'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/BetweenDatesFilterTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/BetweenDatesFilterTest.php new file mode 100644 index 000000000..85db16c72 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/BetweenDatesFilterTest.php @@ -0,0 +1,77 @@ +prophesize(); + + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $this->filter = self::$container->get('chill.calendar.export.between_dates_filter'); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + return [ + [ + 'date_from' => DateTime::createFromFormat('Y-m-d', '2022-05-01'), + 'date_to' => DateTime::createFromFormat('Y-m-d', '2022-06-01'), + ], + ]; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('cal.id') + ->from(Calendar::class, 'cal'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/JobFilterTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/JobFilterTest.php new file mode 100644 index 000000000..61cb47120 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/JobFilterTest.php @@ -0,0 +1,89 @@ +prophesize(); + + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $this->filter = self::$container->get('chill.calendar.export.job_filter'); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + $em = self::$container->get(EntityManagerInterface::class); + + $array = $em->createQueryBuilder() + ->from(UserJob::class, 'uj') + ->select('uj') + ->getQuery() + ->getResult(); + + $data = []; + + foreach ($array as $a) { + $data[] = [ + 'job' => $a, + ]; + } + + return $data; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('cal.id') + ->from(Calendar::class, 'cal') + ->join('cal.user', 'caluser'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/ScopeFilterTest.php b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/ScopeFilterTest.php new file mode 100644 index 000000000..a6ab9ed7b --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Export/Filter/ScopeFilterTest.php @@ -0,0 +1,89 @@ +prophesize(); + + $request->willExtend(\Symfony\Component\HttpFoundation\Request::class); + $request->getLocale()->willReturn('fr'); + + $this->filter = self::$container->get('chill.calendar.export.scope_filter'); + } + + public function getFilter() + { + return $this->filter; + } + + public function getFormData(): array + { + $em = self::$container->get(EntityManagerInterface::class); + + $array = $em->createQueryBuilder() + ->from(Scope::class, 's') + ->select('s') + ->getQuery() + ->getResult(); + + $data = []; + + foreach ($array as $a) { + $data[] = [ + 'scope' => $a, + ]; + } + + return $data; + } + + public function getQueryBuilders(): array + { + if (null === self::$kernel) { + self::bootKernel(); + } + + $em = self::$container->get(EntityManagerInterface::class); + + return [ + $em->createQueryBuilder() + ->select('cal.id') + ->from(Calendar::class, 'cal') + ->join('cal.user', 'caluser'), + ]; + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Form/CalendarTypeTest.php b/src/Bundle/ChillCalendarBundle/Tests/Form/CalendarTypeTest.php index 8adc0599c..3ddc2e836 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Form/CalendarTypeTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Form/CalendarTypeTest.php @@ -1,7 +1,7 @@ entityManager = self::$container->get(EntityManagerInterface::class); + } + + public function testCountByPerosn() + { + $person = $this->getRandomPerson($this->entityManager); + + $periodRepository = $this->prophesize(AccompanyingPeriodACLAwareRepositoryInterface::class); + $periodRepository->findByPerson($person, AccompanyingPeriodVoter::SEE)->willReturn([]); + + $calendarRepository = new CalendarACLAwareRepository( + $periodRepository->reveal(), + $this->entityManager + ); + + $count = $calendarRepository->countByPerson($person, new DateTimeImmutable('yesterday'), new DateTimeImmutable('tomorrow')); + + $this->assertIsInt($count); + } + + /** + * Test that the query does not throw any error. + */ + public function testFindByPerson() + { + $person = $this->getRandomPerson($this->entityManager); + + $periodRepository = $this->prophesize(AccompanyingPeriodACLAwareRepositoryInterface::class); + $periodRepository->findByPerson($person, AccompanyingPeriodVoter::SEE)->willReturn([]); + + $calendarRepository = new CalendarACLAwareRepository( + $periodRepository->reveal(), + $this->entityManager + ); + + $calendars = $calendarRepository->findByPerson($person, null, null, ['startDate' => 'ASC'], 10, 1); + + $this->assertIsArray($calendars); + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/Serializer/Normalizer/CalendarNormalizerTest.php b/src/Bundle/ChillCalendarBundle/Tests/Serializer/Normalizer/CalendarNormalizerTest.php new file mode 100644 index 000000000..2a0975206 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/Serializer/Normalizer/CalendarNormalizerTest.php @@ -0,0 +1,109 @@ +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/Tests/Service/ShortMessageNotification/BulkCalendarShortMessageSenderTest.php b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/BulkCalendarShortMessageSenderTest.php index fc1262d64..0c190f51d 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/BulkCalendarShortMessageSenderTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Service/ShortMessageNotification/BulkCalendarShortMessageSenderTest.php @@ -1,7 +1,7 @@ addSql('ALTER TABLE chill_calendar.calendar DROP urgent'); + } + + public function getDescription(): string + { + return 'Add urgent property to calendar entity'; + } + + public function up(Schema $schema): void + { + // this up() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE chill_calendar.calendar ADD urgent BOOLEAN DEFAULT NULL'); + } +} 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/migrations/Version20221021092541.php b/src/Bundle/ChillCalendarBundle/migrations/Version20221021092541.php new file mode 100644 index 000000000..732b975e3 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/migrations/Version20221021092541.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE chill_calendar.calendar DROP person_id'); + } + + public function getDescription(): string + { + return 'Associate a calendar with a person'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_calendar.calendar ADD person_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_calendar.calendar ADD CONSTRAINT FK_712315AC217BBB47 FOREIGN KEY (person_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_712315AC217BBB47 ON chill_calendar.calendar (person_id)'); + } +} diff --git a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml index b8b1e2e56..60d97d17f 100644 --- a/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillCalendarBundle/translations/messages.fr.yml @@ -50,6 +50,12 @@ 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 + Create for me: Créer un rendez-vous pour moi-même + remote_ms_graph: freebusy_statuses: @@ -72,8 +78,8 @@ invite: # exports Exports of calendar: Exports des rendez-vous -Count appointments: Nombre de rendez-vous -Count appointments by various parameters.: Compte le nombre de rendez-vous en fonction de différents paramètres. +Count calendars: Nombre de rendez-vous +Count calendars by various parameters.: Compte le nombre de rendez-vous en fonction de différents paramètres. Average appointment duration: Moyenne de la durée des rendez-vous Get the average of appointment duration according to various filters: Calcule la moyenne des durées des rendez-vous en fonction de différents paramètres. @@ -82,23 +88,46 @@ Sum of appointment durations: Somme de la durée des rendez-vous Get the sum of appointment durations according to various filters: Calcule la somme des durées des rendez-vous en fonction de différents paramètres. 'Filtered by agent: only %agents%': "Filtré par agents: uniquement %agents%" -Filter appointments by agent: Filtrer les rendez-vous par agents -Filter appointments by agent job: Filtrer les rendez-vous par métiers des agents +Filter calendars by agent: Filtrer les rendez-vous par agents +Filter calendars by agent job: Filtrer les rendez-vous par métiers des agents 'Filtered by agent job: only %jobs%': 'Filtré par métiers des agents: uniquement les %jobs%' -Filter appointments by agent scope: Filtrer les rendez-vous par services des agents +Filter calendars by agent scope: Filtrer les rendez-vous par services des agents 'Filtered by agent scope: only %scopes%': 'Filtré par services des agents: uniquement les services %scopes%' -Filter appointments between certain dates: Filtrer les rendez-vous par date du rendez-vous -'Filtered by appointments between %dateFrom% and %dateTo%': 'Filtré par rendez-vous entre %dateFrom% et %dateTo%' +Filter calendars between certain dates: Filtrer les rendez-vous par date du rendez-vous +'Filtered by calendars between %dateFrom% and %dateTo%': 'Filtré par rendez-vous entre %dateFrom% et %dateTo%' +'Filtered by calendar range: only %calendarRange%': 'Filtré par rendez-vous par plage de disponibilité: uniquement les %calendarRange%' +Filter by calendar range: Filtrer par rendez-vous dans une plage de disponibilité ou non + +Group calendars by agent: Grouper les rendez-vous par agent +Group calendars by agent job: Grouper les rendez-vous par métier de l'agent +Group calendars by agent scope: Grouper les rendez-vous par service de l'agent +Group calendars by location type: Grouper les rendez-vous par type de localisation +Group calendars by location: Grouper les rendez-vous par lieu de rendez-vous +Group calendars by cancel reason: Grouper les rendez-vous par motif d'annulation +Group calendars by month and year: Grouper les rendez-vous par mois et année +Group calendars by urgency: Grouper les rendez-vous par urgent ou non -Group appointments by agent: Grouper les rendez-vous par agent -Group appointments by agent job: Grouper les rendez-vous par métier de l'agent -Group appointments by agent scope: Grouper les rendez-vous par service de l'agent -Group appointments by location type: Grouper les rendez-vous par type de localisation -Group appointments by location: Grouper les rendez-vous par lieu de rendez-vous -Group appointments by cancel reason: Grouper les rendez-vous par motif d'annulation -Group appointments by month and year: Grouper les rendez-vous par mois et année Scope: Service Job: Métier Location type: Type de localisation Location: Lieu de rendez-vous by month and year: Par mois et année + +is urgent: Urgent +is not urgent: Pas urgent +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/Service/Context/BaseContextData.php b/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php index cbeeaeb16..e7b56ed88 100644 --- a/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php +++ b/src/Bundle/ChillDocGeneratorBundle/Service/Context/BaseContextData.php @@ -42,6 +42,9 @@ class BaseContextData $data['createdAt'] = $this->normalizer->normalize(new DateTimeImmutable(), 'docgen', [ 'docgen:expects' => DateTimeImmutable::class, 'groups' => ['docgen:read'], ]); + $data['createdAtDate'] = $this->normalizer->normalize(new DateTimeImmutable('today'), 'docgen', [ + 'docgen:expects' => DateTimeImmutable::class, 'groups' => ['docgen:read'], + ]); $data['location'] = $this->normalizer->normalize( $user instanceof User ? $user->getCurrentLocation() : null, 'docgen', 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/UserController.php b/src/Bundle/ChillMainBundle/Controller/UserController.php index 03d8d2692..9d3941411 100644 --- a/src/Bundle/ChillMainBundle/Controller/UserController.php +++ b/src/Bundle/ChillMainBundle/Controller/UserController.php @@ -15,7 +15,7 @@ use Chill\MainBundle\CRUD\Controller\CRUDController; use Chill\MainBundle\Entity\GroupCenter; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Form\Type\ComposedGroupCenterType; -use Chill\MainBundle\Form\Type\Select2UserLocationType; +use Chill\MainBundle\Form\UserCurrentLocationType; use Chill\MainBundle\Form\UserPasswordType; use Chill\MainBundle\Form\UserType; use Chill\MainBundle\Pagination\PaginatorInterface; @@ -234,7 +234,7 @@ class UserController extends CRUDController public function editCurrentLocationAction(Request $request) { $user = $this->getUser(); - $form = $this->createForm(Select2UserLocationType::class, $user) + $form = $this->createForm(UserCurrentLocationType::class, $user) ->add('submit', SubmitType::class, ['label' => 'Save']) ->handleRequest($request); diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 90ec283df..9164607a7 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -28,6 +28,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; @@ -235,6 +236,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/DependencyInjection/CompilerPass/ExportsCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php index ae85ba22c..5c351728a 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ExportsCompilerPass.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\MainBundle\DependencyInjection\CompilerPass; +use Chill\MainBundle\Export\ExportManager; use LogicException; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -30,53 +31,19 @@ class ExportsCompilerPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!$container->has('Chill\MainBundle\Export\ExportManager')) { - throw new LogicException('service Chill\MainBundle\Export\ExportManager ' + if (!$container->has(ExportManager::class)) { + throw new LogicException('service ' . ExportManager::class . ' ' . 'is not defined. It is required by ExportsCompilerPass'); } $chillManagerDefinition = $container->findDefinition( - 'Chill\MainBundle\Export\ExportManager' + ExportManager::class ); - $this->compileExports($chillManagerDefinition, $container); - $this->compileFilters($chillManagerDefinition, $container); - $this->compileAggregators($chillManagerDefinition, $container); $this->compileFormatters($chillManagerDefinition, $container); $this->compileExportElementsProvider($chillManagerDefinition, $container); } - private function compileAggregators( - Definition $chillManagerDefinition, - ContainerBuilder $container - ) { - $taggedServices = $container->findTaggedServiceIds( - 'chill.export_aggregator' - ); - - $knownAliases = []; - - foreach ($taggedServices as $id => $tagAttributes) { - foreach ($tagAttributes as $attributes) { - if (!isset($attributes['alias'])) { - throw new LogicException("the 'alias' attribute is missing in your " . - "service '{$id}' definition"); - } - - if (array_search($attributes['alias'], $knownAliases, true)) { - throw new LogicException('There is already a chill.export_aggregator service with alias ' - . $attributes['alias'] . '. Choose another alias.'); - } - $knownAliases[] = $attributes['alias']; - - $chillManagerDefinition->addMethodCall( - 'addAggregator', - [new Reference($id), $attributes['alias']] - ); - } - } - } - private function compileExportElementsProvider( Definition $chillManagerDefinition, ContainerBuilder $container @@ -108,68 +75,6 @@ class ExportsCompilerPass implements CompilerPassInterface } } - private function compileExports( - Definition $chillManagerDefinition, - ContainerBuilder $container - ) { - $taggedServices = $container->findTaggedServiceIds( - 'chill.export' - ); - - $knownAliases = []; - - foreach ($taggedServices as $id => $tagAttributes) { - foreach ($tagAttributes as $attributes) { - if (!isset($attributes['alias'])) { - throw new LogicException("the 'alias' attribute is missing in your " . - "service '{$id}' definition"); - } - - if (array_search($attributes['alias'], $knownAliases, true)) { - throw new LogicException('There is already a chill.export service with alias ' - . $attributes['alias'] . '. Choose another alias.'); - } - $knownAliases[] = $attributes['alias']; - - $chillManagerDefinition->addMethodCall( - 'addExport', - [new Reference($id), $attributes['alias']] - ); - } - } - } - - private function compileFilters( - Definition $chillManagerDefinition, - ContainerBuilder $container - ) { - $taggedServices = $container->findTaggedServiceIds( - 'chill.export_filter' - ); - - $knownAliases = []; - - foreach ($taggedServices as $id => $tagAttributes) { - foreach ($tagAttributes as $attributes) { - if (!isset($attributes['alias'])) { - throw new LogicException("the 'alias' attribute is missing in your " . - "service '{$id}' definition"); - } - - if (array_search($attributes['alias'], $knownAliases, true)) { - throw new LogicException('There is already a chill.export_filter service with alias ' - . $attributes['alias'] . '. Choose another alias.'); - } - $knownAliases[] = $attributes['alias']; - - $chillManagerDefinition->addMethodCall( - 'addFilter', - [new Reference($id), $attributes['alias']] - ); - } - } - } - private function compileFormatters( Definition $chillManagerDefinition, ContainerBuilder $container diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php index fa9994408..f75840c3a 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/CompilerPass/ShortMessageCompilerPass.php @@ -1,7 +1,7 @@ >%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/Export/ExportManager.php b/src/Bundle/ChillMainBundle/Export/ExportManager.php index aec5998ff..f39926083 100644 --- a/src/Bundle/ChillMainBundle/Export/ExportManager.php +++ b/src/Bundle/ChillMainBundle/Export/ExportManager.php @@ -14,10 +14,8 @@ namespace Chill\MainBundle\Export; use Chill\MainBundle\Form\Type\Export\ExportType; use Chill\MainBundle\Form\Type\Export\PickCenterType; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; -use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\QueryBuilder; use Generator; -use InvalidArgumentException; use LogicException; use Psr\Log\LoggerInterface; use RuntimeException; @@ -50,8 +48,6 @@ class ExportManager private AuthorizationHelperInterface $authorizationHelper; - private EntityManagerInterface $em; - /** * Collected Exports, injected by DI. * @@ -82,16 +78,28 @@ class ExportManager public function __construct( LoggerInterface $logger, - EntityManagerInterface $em, AuthorizationCheckerInterface $authorizationChecker, AuthorizationHelperInterface $authorizationHelper, - TokenStorageInterface $tokenStorage + TokenStorageInterface $tokenStorage, + iterable $exports, + iterable $aggregators, + iterable $filters + //iterable $formatters, + //iterable $exportElementProvider ) { $this->logger = $logger; - $this->em = $em; $this->authorizationChecker = $authorizationChecker; $this->authorizationHelper = $authorizationHelper; $this->user = $tokenStorage->getToken()->getUser(); + $this->exports = iterator_to_array($exports); + $this->aggregators = iterator_to_array($aggregators); + $this->filters = iterator_to_array($filters); + // NOTE: PHP crashes on the next line (exit error code 11). This is desactivated until further investigation + //$this->formatters = iterator_to_array($formatters); + + //foreach ($exportElementProvider as $prefix => $provider) { + // $this->addExportElementsProvider($provider, $prefix); + //} } /** @@ -141,52 +149,17 @@ class ExportManager } } - /** - * add an aggregator. - * - * @internal used by DI - * - * @param string $alias - */ - public function addAggregator(AggregatorInterface $aggregator, $alias) - { - $this->aggregators[$alias] = $aggregator; - } - - /** - * add an export. - * - * @internal used by DI - * - * @param DirectExportInterface|ExportInterface $export - * @param type $alias - */ - public function addExport($export, $alias) - { - if ($export instanceof ExportInterface || $export instanceof DirectExportInterface) { - $this->exports[$alias] = $export; - } else { - throw new InvalidArgumentException(sprintf( - 'The export with alias %s ' - . 'does not implements %s or %s.', - $alias, - ExportInterface::class, - DirectExportInterface::class - )); - } - } - public function addExportElementsProvider(ExportElementsProviderInterface $provider, $prefix) { foreach ($provider->getExportElements() as $suffix => $element) { $alias = $prefix . '_' . $suffix; if ($element instanceof ExportInterface) { - $this->addExport($element, $alias); + $this->exports[$alias] = $element; } elseif ($element instanceof FilterInterface) { - $this->addFilter($element, $alias); + $this->filters[$alias] = $element; } elseif ($element instanceof AggregatorInterface) { - $this->addAggregator($element, $alias); + $this->aggregators[$alias] = $element; } elseif ($element instanceof FormatterInterface) { $this->addFormatter($element, $alias); } else { @@ -196,24 +169,12 @@ class ExportManager } } - /** - * add a Filter. - * - * @internal Normally used by the dependency injection - * - * @param string $alias - */ - public function addFilter(FilterInterface $filter, $alias) - { - $this->filters[$alias] = $filter; - } - /** * add a formatter. * * @internal used by DI * - * @param type $alias + * @param string $alias */ public function addFormatter(FormatterInterface $formatter, $alias) { @@ -231,7 +192,6 @@ class ExportManager public function generate($exportAlias, array $pickedCentersData, array $data, array $formatterData) { $export = $this->getExport($exportAlias); - //$qb = $this->em->createQueryBuilder(); $centers = $this->getPickedCenters($pickedCentersData); if ($export instanceof DirectExportInterface) { diff --git a/src/Bundle/ChillMainBundle/Form/DataTransformer/IdToEntityDataTransformer.php b/src/Bundle/ChillMainBundle/Form/DataTransformer/IdToEntityDataTransformer.php index 9df6d053d..5cc50f185 100644 --- a/src/Bundle/ChillMainBundle/Form/DataTransformer/IdToEntityDataTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/DataTransformer/IdToEntityDataTransformer.php @@ -1,7 +1,7 @@ translatableStringHelper = $translatableStringHelper; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'class' => LocationType::class, + 'choice_label' => function (LocationType $type) { + return $this->translatableStringHelper->localize($type->getTitle()); + }, + 'placeholder' => 'Pick a location type', + 'required' => false, + 'attr' => ['class' => 'select2'], + 'label' => 'Location type', + 'multiple' => false, + ]) + ->setAllowedTypes('multiple', ['bool']); + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/Select2UserLocationType.php b/src/Bundle/ChillMainBundle/Form/Type/PickUserLocationType.php similarity index 72% rename from src/Bundle/ChillMainBundle/Form/Type/Select2UserLocationType.php rename to src/Bundle/ChillMainBundle/Form/Type/PickUserLocationType.php index 8fb100441..792daa39e 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/Select2UserLocationType.php +++ b/src/Bundle/ChillMainBundle/Form/Type/PickUserLocationType.php @@ -16,10 +16,9 @@ use Chill\MainBundle\Repository\LocationRepository; use Chill\MainBundle\Templating\TranslatableStringHelper; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; -class Select2UserLocationType extends AbstractType +class PickUserLocationType extends AbstractType { private LocationRepository $locationRepository; @@ -31,10 +30,10 @@ class Select2UserLocationType extends AbstractType $this->locationRepository = $locationRepository; } - public function buildForm(FormBuilderInterface $builder, array $options) + public function configureOptions(OptionsResolver $resolver) { - $builder - ->add('currentLocation', EntityType::class, [ + $resolver + ->setDefaults([ 'class' => Location::class, 'choices' => $this->locationRepository->findByPublicLocations(), 'choice_label' => function (Location $entity) { @@ -44,24 +43,15 @@ class Select2UserLocationType extends AbstractType }, 'placeholder' => 'Pick a location', 'required' => false, - 'label' => $options['label'], - 'label_attr' => $options['label_attr'], - 'multiple' => $options['multiple'], 'attr' => ['class' => 'select2'], - ]); - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver - ->setDefault('label', 'Current location') - ->setDefault('label_attr', []) - ->setDefault('multiple', false) + 'label' => 'Current location', + 'multiple' => false, + ]) ->setAllowedTypes('multiple', ['bool']); } - public function getBlockPrefix(): string + public function getParent(): string { - return 'select2_user_location_type'; + return EntityType::class; } } diff --git a/src/Bundle/ChillMainBundle/Form/Type/Select2LocationTypeType.php b/src/Bundle/ChillMainBundle/Form/Type/Select2LocationTypeType.php deleted file mode 100644 index 783ec4e5c..000000000 --- a/src/Bundle/ChillMainBundle/Form/Type/Select2LocationTypeType.php +++ /dev/null @@ -1,59 +0,0 @@ -translatableStringHelper = $translatableStringHelper; - } - - public function buildForm(FormBuilderInterface $builder, array $options) - { - $builder->add('locationtype', EntityType::class, [ - 'class' => LocationType::class, - 'choice_label' => function (LocationType $type) { - return $this->translatableStringHelper->localize($type->getTitle()); - }, - 'placeholder' => 'Pick a location type', - 'required' => false, - 'label' => $options['label'], - 'label_attr' => $options['label_attr'], - 'multiple' => $options['multiple'], - 'attr' => ['class' => 'select2'], - ]); - } - - public function configureOptions(OptionsResolver $resolver) - { - $resolver - ->setDefault('label', 'Location type') - ->setDefault('label_attr', []) - ->setDefault('multiple', false) - ->setAllowedTypes('multiple', ['bool']); - } - - public function getBlockPrefix(): string - { - return 'select2_location_type_type'; - } -} diff --git a/src/Bundle/ChillMainBundle/Form/UserCurrentLocationType.php b/src/Bundle/ChillMainBundle/Form/UserCurrentLocationType.php new file mode 100644 index 000000000..bf6a5d172 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/UserCurrentLocationType.php @@ -0,0 +1,24 @@ +add('currentLocation', PickUserLocationType::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/record_actions.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/record_actions.scss index d72b77e04..b8f60e250 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/record_actions.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/record_actions.scss @@ -5,6 +5,10 @@ ul.record_actions { justify-content: flex-end; padding: 0.5em 0; + &.inline { + display: inline-block; + } + &.column { flex-direction: column; } @@ -18,6 +22,13 @@ ul.record_actions { padding-right: 1em; } + &.small { + .btn { + padding: .25rem .5rem; + font-size: .75rem; + } + } + li { display: inline-block; list-style-type: none; diff --git a/src/Bundle/ChillMainBundle/Service/ShortMessage/NullShortMessageSender.php b/src/Bundle/ChillMainBundle/Service/ShortMessage/NullShortMessageSender.php index 2d5e3e7e2..82dea7bc6 100644 --- a/src/Bundle/ChillMainBundle/Service/ShortMessage/NullShortMessageSender.php +++ b/src/Bundle/ChillMainBundle/Service/ShortMessage/NullShortMessageSender.php @@ -1,7 +1,7 @@ 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/DataTransformer/IdToEntityDataTransformerTest.php b/src/Bundle/ChillMainBundle/Tests/Form/DataTransformer/IdToEntityDataTransformerTest.php index ffcd538cd..2d2cb8e95 100644 --- a/src/Bundle/ChillMainBundle/Tests/Form/DataTransformer/IdToEntityDataTransformerTest.php +++ b/src/Bundle/ChillMainBundle/Tests/Form/DataTransformer/IdToEntityDataTransformerTest.php @@ -1,7 +1,7 @@ context->addViolation($message, ['%phonenumber%' => $value]); + $this->context->addViolation($message, ['%phonenumber%' => $value, '%formatted%' => $this->phonenumberHelper->format($value)]); } } } diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index 10164932f..b44f4b51c 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -91,6 +91,14 @@ services: Chill\MainBundle\Export\ExportManager: autoconfigure: true autowire: true + arguments: + $exports: !tagged_iterator { tag: chill.export, index_by: alias } + $aggregators: !tagged_iterator { tag: chill.export_aggregator, index_by: alias } + $filters: !tagged_iterator { tag: chill.export_filter, index_by: alias } + # for an unknown reason, iterator_to_array($formatter) cause a segmentation fault error (php-fpm code 11). removed temporarily + # $formatters: !tagged_iterator { tag: chill.export_formatter, index_by: alias } + # remove until we can properly test it + # $exportElementProvider: !tagged_iterator { tag: chill.export_elements_provider, index_by: prefix } Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher' diff --git a/src/Bundle/ChillMainBundle/config/services/form.yaml b/src/Bundle/ChillMainBundle/config/services/form.yaml index 26d1f28c0..58f01883f 100644 --- a/src/Bundle/ChillMainBundle/config/services/form.yaml +++ b/src/Bundle/ChillMainBundle/config/services/form.yaml @@ -130,14 +130,6 @@ services: autowire: true autoconfigure: true - Chill\MainBundle\Form\Type\Select2UserLocationType: - autowire: true - autoconfigure: true - - Chill\MainBundle\Form\Type\Select2LocationTypeType: - autowire: true - autoconfigure: true - Chill\MainBundle\Form\Type\LocationFormType: ~ Chill\MainBundle\Form\WorkflowStepType: ~ diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220506223243.php b/src/Bundle/ChillMainBundle/migrations/Version20220506223243.php index c85c4aa35..72d025f2b 100644 --- a/src/Bundle/ChillMainBundle/migrations/Version20220506223243.php +++ b/src/Bundle/ChillMainBundle/migrations/Version20220506223243.php @@ -1,7 +1,7 @@ calendars = new ArrayCollection(); + $this->calendars = new ArrayCollection(); // TODO we cannot add a dependency between AccompanyingPeriod and calendars $this->participations = new ArrayCollection(); $this->scopes = new ArrayCollection(); $this->socialIssues = new ArrayCollection(); diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php index 6dfed9f34..bc439c083 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialAction.php @@ -228,6 +228,22 @@ class SocialAction return $descendants; } + /** + * @param Collection|SocialAction[] $socialActions + */ + public static function getDescendantsWithThisForActions($socialActions): Collection + { + $unique = []; + + foreach ($socialActions as $action) { + foreach ($action->getDescendantsWithThis() as $child) { + $unique[spl_object_hash($child)] = $child; + } + } + + return new ArrayCollection(array_values($unique)); + } + public function getEvaluations(): Collection { return $this->evaluations; @@ -274,6 +290,11 @@ class SocialAction return $this->title; } + public function hasChildren(): bool + { + return 0 < $this->getChildren()->count(); + } + public function hasParent(): bool { return $this->getParent() instanceof self; @@ -369,6 +390,8 @@ class SocialAction { $this->parent = $parent; + $parent->addChild($this); + return $this; } diff --git a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php index 06f05c91f..42c8442c1 100644 --- a/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php +++ b/src/Bundle/ChillPersonBundle/Entity/SocialWork/SocialIssue.php @@ -71,11 +71,17 @@ class SocialIssue $this->socialActions = new ArrayCollection(); } + /** + * @internal use @see{SocialIssue::setParent} instead + * + * @param SocialIssue $child + * + * @return $this + */ public function addChild(self $child): self { if (!$this->children->contains($child)) { $this->children[] = $child; - $child->setParent($this); } return $this; @@ -215,6 +221,22 @@ class SocialIssue return $descendants; } + /** + * @param array|SocialIssue[] $socialIssues + */ + public static function getDescendantsWithThisForIssues(array $socialIssues): Collection + { + $unique = []; + + foreach ($socialIssues as $issue) { + foreach ($issue->getDescendantsWithThis() as $child) { + $unique[spl_object_hash($child)] = $child; + } + } + + return new ArrayCollection(array_values($unique)); + } + public function getId(): ?int { return $this->id; @@ -262,6 +284,11 @@ class SocialIssue return $this->title; } + public function hasChildren(): bool + { + return 0 < $this->getChildren()->count(); + } + public function hasParent(): bool { return null !== $this->parent; @@ -329,6 +356,8 @@ class SocialIssue { $this->parent = $parent; + $parent->addChild($this); + return $this; } diff --git a/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php new file mode 100644 index 000000000..e96c8a228 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Export/Aggregator/AccompanyingCourseAggregators/ByHouseholdCompositionAggregator.php @@ -0,0 +1,131 @@ +householdCompositionTypeRepository = $householdCompositionTypeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function addRole(): ?string + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $p = self::PREFIX; + + if (!in_array('acppart', $qb->getAllAliases(), true)) { + $qb->leftJoin('acp.participations', 'acppart'); + } + + $qb + ->leftJoin( + HouseholdMember::class, + "{$p}_hm", + Join::WITH, + $qb->expr()->orX( + $qb->expr()->isNull("{$p}_hm"), + $qb->expr()->andX( + $qb->expr()->lte("{$p}_hm.startDate", ":{$p}_date"), + $qb->expr()->orX( + $qb->expr()->isNull("{$p}_hm.endDate"), + $qb->expr()->gt("{$p}_hm.endDate", ":{$p}_date") + ) + ) + ) + ) + ->leftJoin( + HouseholdComposition::class, + "{$p}_compo", + Join::WITH, + $qb->expr()->orX( + $qb->expr()->isNull("{$p}_compo"), + $qb->expr()->andX( + $qb->expr()->lte("{$p}_compo.startDate", ":{$p}_date"), + $qb->expr()->orX( + $qb->expr()->isNull("{$p}_compo.endDate"), + $qb->expr()->gt("{$p}_compo.endDate", ":{$p}_date") + ) + ) + ) + ) + ->addSelect("IDENTITY({$p}_compo.householdCompositionType) AS {$p}_select") + ->setParameter("{$p}_date", $data['date_calc']) + ->addGroupBy("{$p}_select"); + } + + public function applyOn() + { + return Declarations::ACP_TYPE; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('date_calc', ChillDateType::class, [ + 'label' => 'export.aggregator.course.by_household_composition.Calc date', + 'input_format' => 'datetime_immutable', + 'data' => new \DateTimeImmutable('now'), + ]); + } + + public function getLabels($key, array $values, $data) + { + return function ($value) { + if ('_header' === $value) { + return 'export.aggregator.course.by_household_composition.Household composition'; + } + + if (null === $value) { + return ''; + } + + if (null === $o = $this->householdCompositionTypeRepository->find($value)) { + return ''; + } + + return $this->translatableStringHelper->localize($o->getLabel()); + }; + } + + public function getQueryKeys($data) + { + return [self::PREFIX . '_select']; + } + + public function getTitle() + { + return 'export.aggregator.course.by_household_composition.Group course by household composition'; + } +} diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php index d289220af..c74e309b2 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/AdministrativeLocationFilter.php @@ -12,7 +12,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters; use Chill\MainBundle\Export\FilterInterface; -use Chill\MainBundle\Form\Type\Select2UserLocationType; +use Chill\MainBundle\Form\Type\PickUserLocationType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Export\Declarations; use Doctrine\ORM\QueryBuilder; @@ -48,11 +48,8 @@ class AdministrativeLocationFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_locations', Select2UserLocationType::class, [ + $builder->add('accepted_locations', PickUserLocationType::class, [ 'label' => 'Accepted locations', - 'label_attr' => [ - //'class' => 'd-none' - ], 'multiple' => true, ]); } @@ -66,7 +63,7 @@ class AdministrativeLocationFilter implements FilterInterface } return ['Filtered by administratives locations: only %locations%', [ - '%locations%' => implode(', ou ', $locations), + '%locations%' => implode(', ', $locations), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php index 00ea8210d..07e61b42e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ClosingMotiveFilter.php @@ -77,7 +77,7 @@ class ClosingMotiveFilter implements FilterInterface return [ 'Filtered by closingmotive: only %closingmotives%', [ - '%closingmotives%' => implode(', ou ', $motives), + '%closingmotives%' => implode(', ', $motives), ], ]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php index 996ff36e3..1839268e1 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/EvaluationFilter.php @@ -87,7 +87,7 @@ class EvaluationFilter implements FilterInterface } return ['Filtered by evaluations: only %evals%', [ - '%evals%' => implode(', ou ', $evaluations), + '%evals%' => implode(', ', $evaluations), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php index 9f87f0ca3..445535734 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/OriginFilter.php @@ -76,7 +76,7 @@ class OriginFilter implements FilterInterface } return ['Filtered by origins: only %origins%', [ - '%origins%' => implode(', ou ', $origins), + '%origins%' => implode(', ', $origins), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php index 8781f6cad..7d1aabc22 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/ReferrerFilter.php @@ -89,7 +89,7 @@ class ReferrerFilter implements FilterInterface return [ 'Filtered by referrer: only %referrers%', [ - '%referrers' => implode(', ou ', $users), + '%referrers' => implode(', ', $users), ], ]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php index c5c37ce21..bc1f368da 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialActionFilter.php @@ -15,10 +15,9 @@ use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Chill\PersonBundle\Export\Declarations; +use Chill\PersonBundle\Form\Type\PickSocialActionType; use Chill\PersonBundle\Templating\Entity\SocialActionRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; use function in_array; @@ -51,17 +50,13 @@ class SocialActionFilter implements FilterInterface $qb->join('acpw.socialAction', 'acpwsocialaction'); } - $where = $qb->getDQLPart('where'); $clause = $qb->expr()->in('acpwsocialaction.id', ':socialactions'); - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter('socialactions', $data['accepted_socialactions']); + $qb->andWhere($clause) + ->setParameter( + 'socialactions', + SocialAction::getDescendantsWithThisForActions($data['accepted_socialactions'])->toArray() + ); } public function applyOn(): string @@ -71,26 +66,25 @@ class SocialActionFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_socialactions', EntityType::class, [ - 'class' => SocialAction::class, - 'choice_label' => function (SocialAction $sa) { - return $this->actionRender->renderString($sa, []); - }, + $builder->add('accepted_socialactions', PickSocialActionType::class, [ 'multiple' => true, - 'expanded' => true, ]); } public function describeAction($data, $format = 'string'): array { - $socialactions = []; + $actions = []; - foreach ($data['accepted_socialactions'] as $sa) { - $socialactions[] = $this->actionRender->renderString($sa, []); + $socialactions = $data['accepted_socialactions']; + + foreach ($socialactions as $action) { + $actions[] = $this->actionRender->renderString($action, [ + 'show_and_children' => true, + ]); } return ['Filtered by socialactions: only %socialactions%', [ - '%socialactions%' => implode(', ou ', $socialactions), + '%socialactions%' => implode(', ', $actions), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php index d896d6395..141a1a2db 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/AccompanyingCourseFilters/SocialIssueFilter.php @@ -15,10 +15,9 @@ use Chill\MainBundle\Export\FilterInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Chill\PersonBundle\Export\Declarations; +use Chill\PersonBundle\Form\Type\PickSocialIssueType; use Chill\PersonBundle\Templating\Entity\SocialIssueRender; -use Doctrine\ORM\Query\Expr\Andx; use Doctrine\ORM\QueryBuilder; -use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Contracts\Translation\TranslatorInterface; use function in_array; @@ -55,20 +54,13 @@ class SocialIssueFilter implements FilterInterface $qb->join('acp.socialIssues', 'acpsocialissue'); } - $where = $qb->getDQLPart('where'); $clause = $qb->expr()->in('acpsocialissue.id', ':socialissues'); - if ($where instanceof Andx) { - $where->add($clause); - } else { - $where = $qb->expr()->andX($clause); - } - - $qb->add('where', $where); - $qb->setParameter( - 'socialissues', - $this->addParentIssues($data['accepted_socialissues']) - ); + $qb->andWhere($clause) + ->setParameter( + 'socialissues', + SocialIssue::getDescendantsWithThisForIssues($data['accepted_socialissues']) + ); } public function applyOn() @@ -78,74 +70,31 @@ class SocialIssueFilter implements FilterInterface public function buildForm(FormBuilderInterface $builder) { - $builder->add('accepted_socialissues', EntityType::class, [ - 'class' => SocialIssue::class, - 'choice_label' => function ($socialIssue) { - return $this->socialIssueRender->renderString($socialIssue, []); - }, + $builder->add('accepted_socialissues', PickSocialIssueType::class, [ 'multiple' => true, - 'expanded' => true, ]); } - public function describeAction($data, $format = 'string') + public function describeAction($data, $format = 'string'): array { $issues = []; - $socialissues = $this->addParentIssues($data['accepted_socialissues']); + $socialissues = $data['accepted_socialissues']; - foreach ($socialissues as $i) { - if ('null' === $i) { - $issues[] = $this->translator->trans('Not given'); - } else { - $issues[] = $this->socialIssueRender->renderString($i, []); - } + foreach ($socialissues as $issue) { + $issues[] = $this->socialIssueRender->renderString($issue, [ + 'show_and_children' => true, + ]); } return [ 'Filtered by socialissues: only %socialissues%', [ - '%socialissues%' => implode(', ou ', $issues), + '%socialissues%' => implode(', ', $issues), ], ]; } - public function getTitle() + public function getTitle(): string { return 'Filter by social issue'; } - - /** - * "Le filtre retiendra les parcours qui comportent cette problématique, - * ou une problématique parente à celles choisies.". - * - * Add parent of each socialissue selected, and remove duplicates - * - * @param $accepted_issues - */ - private function addParentIssues($accepted_issues): array - { - $array = []; - - foreach ($accepted_issues as $i) { - /** @var SocialIssue $i */ - if ($i->hasParent()) { - $array[] = $i->getParent(); - } - $array[] = $i; - } - - return $this->removeDuplicate($array); - } - - private function removeDuplicate(array $array): array - { - $ids = array_map(static function ($item) { - return $item->getId(); - }, $array); - - $unique_ids = array_unique($ids); - - return array_values( - array_intersect_key($array, $unique_ids) - ); - } } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php index 2a5c143c7..65e38f41e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/EvaluationFilters/EvaluationTypeFilter.php @@ -76,7 +76,7 @@ final class EvaluationTypeFilter implements FilterInterface } return ['Filtered by evaluation type: only %evals%', [ - '%evals%' => implode(', ou ', $evals), + '%evals%' => implode(', ', $evals), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php index 9296717ce..3ff8cb63e 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/HouseholdFilters/CompositionFilter.php @@ -95,7 +95,7 @@ class CompositionFilter implements FilterInterface } return ['Filtered by composition: only %compositions% on %ondate%', [ - '%compositions%' => implode(', ou ', $compositions), + '%compositions%' => implode(', ', $compositions), '%ondate%' => $data['on_date']->format('d-m-Y'), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php index fd8e14c83..a69e1ac76 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/JobFilter.php @@ -90,7 +90,7 @@ class JobFilter implements FilterInterface } return ['Filtered by treating agent job: only %jobs%', [ - '%jobs%' => implode(', ou ', $userjobs), + '%jobs%' => implode(', ', $userjobs), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php index 65d97cbf1..febc57a17 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ReferrerFilter.php @@ -81,7 +81,7 @@ class ReferrerFilter implements FilterInterface return [ 'Filtered by treating agent: only %agents%', [ - '%agents' => implode(', ou ', $users), + '%agents' => implode(', ', $users), ], ]; } diff --git a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php index 6d130ce75..8cb2f6421 100644 --- a/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php +++ b/src/Bundle/ChillPersonBundle/Export/Filter/SocialWorkFilters/ScopeFilter.php @@ -90,7 +90,7 @@ class ScopeFilter implements FilterInterface } return ['Filtered by treating agent scope: only %scopes%', [ - '%scopes%' => implode(', ou ', $scopes), + '%scopes%' => implode(', ', $scopes), ]]; } diff --git a/src/Bundle/ChillPersonBundle/Form/DataTransformer/PersonsToIdDataTransformer.php b/src/Bundle/ChillPersonBundle/Form/DataTransformer/PersonsToIdDataTransformer.php index b42642a58..d1cb87e02 100644 --- a/src/Bundle/ChillPersonBundle/Form/DataTransformer/PersonsToIdDataTransformer.php +++ b/src/Bundle/ChillPersonBundle/Form/DataTransformer/PersonsToIdDataTransformer.php @@ -1,7 +1,7 @@ actionRender = $actionRender; + $this->actionRepository = $actionRepository; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'class' => SocialAction::class, + 'choices' => $this->actionRepository->findAllActive(), + 'choice_label' => function (SocialAction $sa) { + return $this->actionRender->renderString($sa, []); + }, + 'placeholder' => 'Pick a social action', + 'required' => false, + 'attr' => ['class' => 'select2'], + 'label' => 'Social actions', + 'multiple' => false, + ]) + ->setAllowedTypes('multiple', ['bool']); + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Form/Type/PickSocialIssueType.php b/src/Bundle/ChillPersonBundle/Form/Type/PickSocialIssueType.php new file mode 100644 index 000000000..e79c17191 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Form/Type/PickSocialIssueType.php @@ -0,0 +1,57 @@ +issueRender = $issueRender; + $this->issueRepository = $issueRepository; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'class' => SocialIssue::class, + 'choices' => $this->issueRepository->findAllActive(), + 'choice_label' => function (SocialIssue $si) { + return $this->issueRender->renderString($si, []); + }, + 'placeholder' => 'Pick a social issue', + 'required' => false, + 'attr' => ['class' => 'select2'], + 'label' => 'Social issues', + 'multiple' => false, + ]) + ->setAllowedTypes('multiple', ['bool']); + } + + public function getParent(): string + { + return EntityType::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php index 453d32a4d..e25d0c867 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php @@ -15,9 +15,8 @@ use Chill\PersonBundle\Entity\Household\Household; use Chill\PersonBundle\Entity\Household\HouseholdComposition; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; -use Doctrine\Persistence\ObjectRepository; -class HouseholdCompositionRepository implements ObjectRepository +final class HouseholdCompositionRepository implements HouseholdCompositionRepositoryInterface { private EntityRepository $repository; diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepositoryInterface.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepositoryInterface.php new file mode 100644 index 000000000..84c62392b --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepositoryInterface.php @@ -0,0 +1,45 @@ +repository->findAll(); } + /** + * @return array|SocialAction[] + */ + public function findAllActive(): array + { + return $this->buildQueryWithDesactivatedDateCriteria()->getQuery()->getResult(); + } + /** * @param mixed|null $limit * @param mixed|null $offset @@ -67,4 +76,16 @@ final class SocialActionRepository implements ObjectRepository { return SocialAction::class; } + + private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('sa'); + + $qb->where('sa.desactivationDate is null') + ->orWhere('sa.desactivationDate > :now') + ->orderBy('sa.ordering', 'ASC') + ->setParameter('now', new DateTime('now')); + + return $qb; + } } diff --git a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php index 27b4ed4e9..8dd8f6a62 100644 --- a/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/SocialWork/SocialIssueRepository.php @@ -12,8 +12,10 @@ declare(strict_types=1); namespace Chill\PersonBundle\Repository\SocialWork; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; +use DateTime; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; use Doctrine\Persistence\ObjectRepository; final class SocialIssueRepository implements ObjectRepository @@ -38,6 +40,14 @@ final class SocialIssueRepository implements ObjectRepository return $this->repository->findAll(); } + /** + * @return array|SocialIssue[] + */ + public function findAllActive(): array + { + return $this->buildQueryWithDesactivatedDateCriteria()->getQuery()->getResult(); + } + /** * @param mixed|null $limit * @param mixed|null $offset @@ -61,4 +71,16 @@ final class SocialIssueRepository implements ObjectRepository { return SocialIssue::class; } + + private function buildQueryWithDesactivatedDateCriteria(): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('si'); + + $qb->where('si.desactivationDate is null') + ->orWhere('si.desactivationDate > :now') + ->orderBy('si.ordering', 'ASC') + ->setParameter('now', new DateTime('now')); + + return $qb; + } } diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php index a9ebdc0bb..af6c6e114 100644 --- a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContext.php @@ -11,8 +11,6 @@ declare(strict_types=1); namespace Chill\PersonBundle\Service\DocGenerator; -use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface; -use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface; use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException; use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate; use Chill\DocGeneratorBundle\Service\Context\BaseContextData; @@ -34,6 +32,7 @@ use Doctrine\ORM\EntityRepository; use LogicException; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -42,7 +41,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; use function array_key_exists; use function count; -class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGeneratorContextWithPublicFormInterface +final class PersonContext implements PersonContextInterface { private AuthorizationHelperInterface $authorizationHelper; @@ -129,6 +128,7 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGen 'choice_label' => function ($entity = null) { return $entity ? $this->translatableStringHelper->localize($entity->getName()) : ''; }, + 'required' => true, ]); } @@ -137,11 +137,19 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGen */ public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void { - $builder->add('scope', ScopePickerType::class, [ - 'center' => $this->centerResolverManager->resolveCenters($entity), - 'role' => PersonDocumentVoter::CREATE, - 'label' => 'Scope', + $builder->add('title', TextType::class, [ + 'required' => true, + 'label' => 'docgen.Document title', + 'data' => $this->translatableStringHelper->localize($template->getName()), ]); + + if ($this->isScopeNecessary($entity)) { + $builder->add('scope', ScopePickerType::class, [ + 'center' => $this->centerResolverManager->resolveCenters($entity), + 'role' => PersonDocumentVoter::CREATE, + 'label' => 'Scope', + ]); + } } public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array @@ -200,7 +208,7 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGen */ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool { - return $this->isScopeNecessary($entity); + return true; } /** @@ -210,7 +218,9 @@ class PersonContext implements DocGeneratorContextWithAdminFormInterface, DocGen { $doc = new PersonDocument(); $doc->setTemplate($template) - ->setTitle($this->translatableStringHelper->localize($template->getName())) + ->setTitle( + $contextGenerationData['title'] ?? $this->translatableStringHelper->localize($template->getName()) + ) ->setDate(new DateTime()) ->setDescription($this->translatableStringHelper->localize($template->getName())) ->setPerson($entity) diff --git a/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContextInterface.php b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContextInterface.php new file mode 100644 index 000000000..58a6b5863 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Service/DocGenerator/PersonContextInterface.php @@ -0,0 +1,55 @@ +personContext = $personContext; + $this->normalizer = $normalizer; + } + + public function adminFormReverseTransform(array $data): array + { + return array_merge( + $this->personContext->adminFormReverseTransform($data), + ['label' => $data['label']] + ); + } + + public function adminFormTransform(array $data): array + { + return array_merge( + $this->personContext->adminFormTransform($data), + ['label' => $data['label'] ?? ''] + ); + } + + public function buildAdminForm(FormBuilderInterface $builder): void + { + $this->personContext->buildAdminForm($builder); + + $builder->add('label', TextType::class, [ + 'label' => 'docgen.Label for third party', + 'required' => true, + ]); + } + + public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void + { + $this->personContext->buildPublicForm($builder, $template, $entity); + + $builder->add('thirdParty', PickThirdpartyDynamicType::class, [ + 'multiple' => false, + 'label' => $template->getOptions()['label'] ?? 'ThirdParty', + 'validation_groups' => ['__none__'], + ]); + } + + public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array + { + $data = $this->personContext->getData($template, $entity, $contextGenerationData); + + $data['thirdParty'] = $this->normalizer->normalize( + $contextGenerationData['thirdParty'], + 'docgen', + ['docgen:expects' => ThirdParty::class, 'groups' => ['docgen:read']] + ); + + return $data; + } + + public function getDescription(): string + { + return 'docgen.A context for person with a third party (for sending mail)'; + } + + public function getEntityClass(): string + { + return $this->personContext->getEntityClass(); + } + + public function getFormData(DocGeneratorTemplate $template, $entity): array + { + return $this->personContext->getFormData($template, $entity); + } + + public static function getKey(): string + { + return self::class; + } + + public function getName(): string + { + return 'docgen.Person with third party'; + } + + public function hasAdminForm(): bool + { + return true; + } + + public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool + { + return true; + } + + public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void + { + $this->personContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData); + } +} diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php index f2ee13fc1..bfe49c0a4 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialActionRender.php @@ -15,16 +15,20 @@ use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\SocialAction; use Symfony\Component\Templating\EngineInterface; - +use Symfony\Contracts\Translation\TranslatorInterface; use function array_merge; use function array_reverse; use function implode; class SocialActionRender implements ChillEntityRenderInterface { + public const AND_CHILDREN_MENTION = 'show_and_children_mention'; + public const DEFAULT_ARGS = [ self::SEPARATOR_KEY => ' > ', self::NO_BADGE => false, + self::SHOW_AND_CHILDREN => false, + self::AND_CHILDREN_MENTION => 'social_action.and children', ]; /** @@ -34,14 +38,26 @@ class SocialActionRender implements ChillEntityRenderInterface public const SEPARATOR_KEY = 'default.separator'; + /** + * Show a mention "and children" on each SocialAction, if the social action + * has at least one child. + */ + public const SHOW_AND_CHILDREN = 'show_and_children'; + private EngineInterface $engine; private TranslatableStringHelper $translatableStringHelper; - public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine) - { + private TranslatorInterface $translator; + + public function __construct( + TranslatableStringHelper $translatableStringHelper, + EngineInterface $engine, + TranslatorInterface $translator + ) { $this->translatableStringHelper = $translatableStringHelper; $this->engine = $engine; + $this->translator = $translator; } public function renderBox($socialAction, array $options): string @@ -72,7 +88,13 @@ class SocialActionRender implements ChillEntityRenderInterface $titles = array_reverse($titles); - return implode($options[self::SEPARATOR_KEY], $titles); + $title = implode($options[self::SEPARATOR_KEY], $titles); + + if ($options[self::SHOW_AND_CHILDREN] && $socialAction->hasChildren()) { + $title .= ' (' . $this->translator->trans($options[self::AND_CHILDREN_MENTION]) . ')'; + } + + return $title; } public function supports($entity, array $options): bool diff --git a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php index 014b23c8e..62d17f47e 100644 --- a/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php +++ b/src/Bundle/ChillPersonBundle/Templating/Entity/SocialIssueRender.php @@ -15,26 +15,42 @@ use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\PersonBundle\Entity\SocialWork\SocialIssue; use Symfony\Component\Templating\EngineInterface; - +use Symfony\Contracts\Translation\TranslatorInterface; use function array_reverse; use function implode; final class SocialIssueRender implements ChillEntityRenderInterface { + public const AND_CHILDREN_MENTION = 'show_and_children_mention'; + public const DEFAULT_ARGS = [ self::SEPARATOR_KEY => ' > ', + self::SHOW_AND_CHILDREN => false, + self::AND_CHILDREN_MENTION => 'social_issue.and children', ]; public const SEPARATOR_KEY = 'default.separator'; + /** + * Show a mention "and children" on each SocialIssue, if the social issue + * has at least one child. + */ + public const SHOW_AND_CHILDREN = 'show_and_children'; + private EngineInterface $engine; private TranslatableStringHelper $translatableStringHelper; - public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine) - { + private TranslatorInterface $translator; + + public function __construct( + TranslatableStringHelper $translatableStringHelper, + EngineInterface $engine, + TranslatorInterface $translator + ) { $this->translatableStringHelper = $translatableStringHelper; $this->engine = $engine; + $this->translator = $translator; } /** @@ -78,7 +94,13 @@ final class SocialIssueRender implements ChillEntityRenderInterface $titles = array_reverse($titles); - return implode($options[self::SEPARATOR_KEY], $titles); + $title = implode($options[self::SEPARATOR_KEY], $titles); + + if ($options[self::SHOW_AND_CHILDREN] && $socialIssue->hasChildren()) { + $title .= ' (' . $this->translator->trans($options[self::AND_CHILDREN_MENTION]) . ')'; + } + + return $title; } public function supports($entity, array $options): bool diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialActionTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialActionTest.php new file mode 100644 index 000000000..2dcfa6b11 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialActionTest.php @@ -0,0 +1,51 @@ +setParent($parentA); + $grandChildA = (new SocialAction())->setParent($childA); + $grandGrandChildA = (new SocialAction())->setParent($grandChildA); + $unrelatedA = new SocialAction(); + + $parentB = new SocialAction(); + $childB = (new SocialAction())->setParent($parentB); + $grandChildB = (new SocialAction())->setParent($childB); + $grandGrandChildB = (new SocialAction())->setParent($grandChildB); + $unrelatedB = new SocialAction(); + + $actual = SocialAction::getDescendantsWithThisForActions([$parentA, $parentB]); + + $this->assertContains($parentA, $actual); + $this->assertContains($parentB, $actual); + $this->assertContains($childA, $actual); + $this->assertContains($childB, $actual); + $this->assertContains($grandChildA, $actual); + $this->assertContains($grandChildB, $actual); + $this->assertContains($grandGrandChildA, $actual); + $this->assertContains($grandGrandChildB, $actual); + $this->assertCount(8, $actual); + $this->assertNotContains($unrelatedA, $actual); + $this->assertNotContains($unrelatedB, $actual); + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialIssueTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialIssueTest.php index edc1e5474..b846ae5f4 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialIssueTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/SocialWork/SocialIssueTest.php @@ -55,6 +55,35 @@ final class SocialIssueTest extends TestCase $this->assertCount(0, $unrelated->getAncestors(false)); } + public function testGetDescendantsWithThisForIssues() + { + $parentA = new SocialIssue(); + $childA = (new SocialIssue())->setParent($parentA); + $grandChildA = (new SocialIssue())->setParent($childA); + $grandGrandChildA = (new SocialIssue())->setParent($grandChildA); + $unrelatedA = new SocialIssue(); + + $parentB = new SocialIssue(); + $childB = (new SocialIssue())->setParent($parentB); + $grandChildB = (new SocialIssue())->setParent($childB); + $grandGrandChildB = (new SocialIssue())->setParent($grandChildB); + $unrelatedB = new SocialIssue(); + + $actual = SocialIssue::getDescendantsWithThisForIssues([$parentA, $parentB]); + + $this->assertContains($parentA, $actual); + $this->assertContains($parentB, $actual); + $this->assertContains($childA, $actual); + $this->assertContains($childB, $actual); + $this->assertContains($grandChildA, $actual); + $this->assertContains($grandChildB, $actual); + $this->assertContains($grandGrandChildA, $actual); + $this->assertContains($grandGrandChildB, $actual); + $this->assertCount(8, $actual); + $this->assertNotContains($unrelatedA, $actual); + $this->assertNotContains($unrelatedB, $actual); + } + public function testIsDescendantOf() { $parent = new SocialIssue(); diff --git a/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/AccompanyingPeriodResourceNormalizerTest.php b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/AccompanyingPeriodResourceNormalizerTest.php new file mode 100644 index 000000000..0c4188826 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Serializer/Normalizer/AccompanyingPeriodResourceNormalizerTest.php @@ -0,0 +1,67 @@ +normalizer = self::$container->get(NormalizerInterface::class); + } + + public function testNormalizeNullHasSameValueAsNotNull() + { + $nullResource = $this->normalizer->normalize(null, 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]); + $notNull = $this->normalizer->normalize(new Resource(), 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]); + + $this->assertEqualsCanonicalizing(array_keys($notNull), array_keys($nullResource)); + } + + public function testNormalizeResource() + { + $resource = new Resource(); + $resource + ->setComment('blabla') + ->setResource(new ThirdParty()); + + $expected = [ + 'type' => 'accompanying_period_resource', + 'isNull' => false, + 'comment' => 'blabla', + ]; + + $actual = $this->normalizer->normalize($resource, 'docgen', ['groups' => 'docgen:read', 'docgen:expects' => Resource::class]); + + // we do not test for sub array (person, thirdparty). We then check first for base value... + foreach ($expected as $key => $value) { + $this->assertArrayHasKey($key, $actual); + $this->assertEquals($value, $actual[$key]); + } + + // ... and then for the existence of some values + $this->assertArrayHasKey('person', $actual); + $this->assertArrayHasKey('thirdParty', $actual); + } +} diff --git a/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextTest.php b/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextTest.php index c7c9a00e4..44414cdba 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextTest.php @@ -21,6 +21,7 @@ use Chill\DocStoreBundle\Security\Authorization\PersonDocumentVoter; use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Form\Type\ScopePickerType; use Chill\MainBundle\Security\Authorization\AuthorizationHelperInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface; @@ -33,6 +34,8 @@ use Prophecy\Exception\Prediction\FailedPredictionException; use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -82,7 +85,7 @@ final class PersonContextTest extends TestCase $parameter ); - $this->assertFalse($personContext->hasPublicForm($docGen, $person)); + $personContext->buildPublicForm($this->buildFormBuilder(false), $docGen, $person); $personContext->storeGenerated( $docGen, @@ -126,7 +129,7 @@ final class PersonContextTest extends TestCase $em->reveal(), ); - $this->assertTrue($personContext->hasPublicForm($docGen, $person)); + $personContext->buildPublicForm($this->buildFormBuilder(true), $docGen, $person); $personContext->storeGenerated( $docGen, @@ -170,7 +173,7 @@ final class PersonContextTest extends TestCase $em->reveal(), ); - $this->assertTrue($personContext->hasPublicForm($docGen, $person)); + $personContext->buildPublicForm($this->buildFormBuilder(true), $docGen, $person); $personContext->storeGenerated( $docGen, @@ -180,6 +183,24 @@ final class PersonContextTest extends TestCase ); } + private function buildFormBuilder(bool $withScope): FormBuilderInterface + { + $builder = $this->prophesize(FormBuilderInterface::class); + + $builder->add('title', TextType::class, Argument::type('array')) + ->shouldBeCalled(1); + + if ($withScope) { + $builder->add('scope', ScopePickerType::class, Argument::type('array')) + ->shouldBeCalled(); + } else { + $builder->add('scope', ScopePickerType::class, Argument::type('array')) + ->shouldNotBeCalled(); + } + + return $builder->reveal(); + } + private function buildPersonContext( ?AuthorizationHelperInterface $authorizationHelper = null, ?BaseContextData $baseContextData = null, diff --git a/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextWithThirdPartyTest.php b/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextWithThirdPartyTest.php new file mode 100644 index 000000000..a4f08e7c1 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Service/DocGenerator/PersonContextWithThirdPartyTest.php @@ -0,0 +1,93 @@ +buildPersonContextWithThirdParty(); + + $actual = $personContext->adminFormReverseTransform(['label' => 'bloup']); + + $this->assertArrayHasKey('category', $actual); + $this->assertArrayHasKey('label', $actual); + $this->assertEquals('bloup', $actual['label']); + } + + public function testAdminFormTransform() + { + $personContext = $this->buildPersonContextWithThirdParty(); + + $actual = $personContext->adminFormTransform(['label' => 'bloup']); + + $this->assertArrayHasKey('from_person', $actual); + $this->assertArrayHasKey('label', $actual); + $this->assertEquals('bloup', $actual['label']); + } + + public function testGetData() + { + $personContext = $this->buildPersonContextWithThirdParty(); + + $actual = $personContext->getData( + (new DocGeneratorTemplate())->setOptions(['label' => 'bloup']), + new Person(), + ['thirdParty' => $tp = new ThirdParty()] + ); + + $this->assertArrayHasKey('person', $actual); + $this->assertArrayHasKey('thirdParty', $actual); + $this->assertEquals(spl_object_hash($tp), $actual['thirdParty']['hash']); + } + + private function buildPersonContextWithThirdParty(): PersonContextWithThirdParty + { + $normalizer = $this->prophesize(NormalizerInterface::class); + $normalizer->normalize(Argument::type(ThirdParty::class), 'docgen', Argument::type('array')) + ->will(static function ($args): array { + return ['class' => '3party', 'hash' => spl_object_hash($args[0])]; + }); + + $personContext = $this->prophesize(PersonContextInterface::class); + + $personContext->adminFormReverseTransform(Argument::type('array'))->willReturn( + ['category' => ['idInsideBundle' => 1, 'bundleId' => 'abc']] + ); + $personContext->adminFormTransform(Argument::type('array'))->willReturn( + ['from_person' => 'kept'] + ); + $personContext->getData(Argument::type(DocGeneratorTemplate::class), Argument::type(Person::class), Argument::type('array')) + ->willReturn(['person' => 'data']); + + return new PersonContextWithThirdParty( + $personContext->reveal(), + $normalizer->reveal() + ); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml index ba79b4166..04d5d8371 100644 --- a/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/exports_accompanying_course.yaml @@ -1,262 +1,193 @@ services: + _defaults: + autowire: true + autoconfigure: true ## Indicators chill.person.export.count_accompanyingcourse: class: Chill\PersonBundle\Export\Export\CountAccompanyingCourse - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: count_accompanyingcourse } chill.person.export.avg_accompanyingcourse_duration: class: Chill\PersonBundle\Export\Export\StatAccompanyingCourseDuration - autowire: true - autoconfigure: true tags: - { name: chill.export, alias: avg_accompanyingcourse_duration } ## Filters Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserScopeFilter: - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_userscope_filter } Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\UserJobFilter: - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_userjob_filter } chill.person.export.filter_socialissue: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\SocialIssueFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_socialissue_filter } chill.person.export.filter_step: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\StepFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_step_filter } chill.person.export.filter_geographicalunitstat: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\GeographicalUnitStatFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_geographicalunitstat_filter } chill.person.export.filter_socialaction: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\SocialActionFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_socialaction_filter } chill.person.export.filter_evaluation: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\EvaluationFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_evaluation_filter } chill.person.export.filter_origin: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OriginFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_origin_filter } chill.person.export.filter_closingmotive: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ClosingMotiveFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_closingmotive_filter } chill.person.export.filter_administrative_location: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\AdministrativeLocationFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_administrative_location_filter } chill.person.export.filter_requestor: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\RequestorFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_requestor_filter } chill.person.export.filter_confidential: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ConfidentialFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_confidential_filter } chill.person.export.filter_emergency: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\EmergencyFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_emergency_filter } chill.person.export.filter_intensity: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\IntensityFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_intensity_filter } chill.person.export.filter_activeondate: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ActiveOnDateFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_activeondate_filter } chill.person.export.filter_activeonedaybetweendates: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ActiveOneDayBetweenDatesFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_activeonedaybetweendates_filter } chill.person.export.filter_referrer: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\ReferrerFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_referrer_filter } chill.person.export.filter_openbetweendates: class: Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters\OpenBetweenDatesFilter - autowire: true - autoconfigure: true tags: - { name: chill.export_filter, alias: accompanyingcourse_openbetweendates_filter } ## Aggregators chill.person.export.aggregator_referrer_scope: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ScopeAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_scope_aggregator } chill.person.export.aggregator_referrer_job: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\JobAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_referrer_job_aggregator } chill.person.export.aggregator_socialissue: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\SocialIssueAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_socialissue_aggregator } chill.person.export.aggregator_step: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\StepAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_step_aggregator } chill.person.export.aggregator_geographicalunitstat: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\GeographicalUnitStatAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_geographicalunitstat_aggregator } chill.person.export.aggregator_socialaction: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\SocialActionAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_socialaction_aggregator } chill.person.export.aggregator_evaluation: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\EvaluationAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_evaluation_aggregator } chill.person.export.aggregator_origin: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\OriginAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_origin_aggregator } chill.person.export.aggregator_closingmotive: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ClosingMotiveAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_closingmotive_aggregator } chill.person.export.aggregator_administrative_location: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\AdministrativeLocationAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_administrative_location_aggregator } chill.person.export.aggregator_requestor: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\RequestorAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_requestor_aggregator } chill.person.export.aggregator_confidential: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ConfidentialAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_confidential_aggregator } chill.person.export.aggregator_emergency: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\EmergencyAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_emergency_aggregator } chill.person.export.aggregator_intensity: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\IntensityAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_intensity_aggregator } chill.person.export.aggregator_referrer: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_referrer_aggregator } chill.person.export.aggregator_duration: class: Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\DurationAggregator - autowire: true - autoconfigure: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_duration_aggregator } Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ReferrerScopeAggregator: - autoconfigure: true - autowire: true tags: - { name: chill.export_aggregator, alias: accompanyingcourse_ref_scope_aggregator } - + Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators\ByHouseholdCompositionAggregator: + tags: + - { name: chill.export_aggregator, alias: accompanyingcourse_by_household_compo_aggregator } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 76fb12d18..f3fd6c430 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -205,9 +205,11 @@ Resources: Interlocuteurs privilégiés Any requestor to this accompanying course: Aucun demandeur pour ce parcours Social action: Action d'accompagnement Social actions: Actions d'accompagnement +Pick a social action: Choisir une action d'accompagnement Last social actions: Les dernières actions d'accompagnement Social issue: Problématique sociale Social issues: Problématiques sociales +Pick a social issue: Choisir une problématique sociale Last events on accompanying course: Dernières actions de suivi Edit & activate accompanying course: Modifier et valider See accompanying periods: Voir toutes les périodes d'accompagnement @@ -887,6 +889,10 @@ docgen: A context for accompanying period work evaluation: Contexte pour les évaluations dans les actions d'accompagnement Person basic: Personne (basique) A basic context for person: Contexte pour les personnes + Person with third party: Personne avec choix d'un tiers + A context for person with a third party (for sending mail): Un contexte d'une personne avec un tiers (pour envoyer un courrier à ce tiers, par exemple) + Label for third party: Label à afficher aux utilisateurs + Document title: Titre du document généré period_notification: period_designated_subject: Vous êtes référent d'un parcours d'accompagnement @@ -958,6 +964,10 @@ export: week: Durée du parcours en semaines month: Durée du parcours en mois Precision: Unité de la durée + by_household_composition: + Household composition: Composition du ménage + Group course by household composition: Grouper les parcours par composition familiale des ménages des usagers concernés + Calc date: Date de calcul de la composition du ménage filter: course: by_user_scope: @@ -965,3 +975,8 @@ export: by_referrer: Computation date for referrer: Date à laquelle le référent était actif +social_action: + and children: et dérivés + +social_issue: + and children: et dérivés diff --git a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php index 119afff11..2e8cebfb6 100644 --- a/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php +++ b/src/Bundle/ChillThirdPartyBundle/Entity/ThirdParty.php @@ -214,7 +214,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface /** * @ORM\Column(name="kind", type="string", length="20", options={"default": ""}) - * @Groups({"write"}) + * @Groups({"write", "docgen:read", "docgen:read:3party:parent"}) */ private ?string $kind = ''; diff --git a/src/Bundle/ChillThirdPartyBundle/Form/DataTransformer/ThirdPartiesToIdDataTransformer.php b/src/Bundle/ChillThirdPartyBundle/Form/DataTransformer/ThirdPartiesToIdDataTransformer.php index 8be0cd81c..c08e4ab05 100644 --- a/src/Bundle/ChillThirdPartyBundle/Form/DataTransformer/ThirdPartiesToIdDataTransformer.php +++ b/src/Bundle/ChillThirdPartyBundle/Form/DataTransformer/ThirdPartiesToIdDataTransformer.php @@ -1,7 +1,7 @@