From 94c91d5825713c9118a5df47cb372e7cc224f310 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 13:09:38 +0200 Subject: [PATCH 1/6] simplifiy filter order --- .../Templating/Listing/FilterOrderHelperBuilder.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php index e1df09827..ec0f6b60b 100644 --- a/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php +++ b/src/Bundle/ChillMainBundle/Templating/Listing/FilterOrderHelperBuilder.php @@ -19,7 +19,7 @@ class FilterOrderHelperBuilder $this->requestStack = $requestStack; } - public function addSearchBox(array $fields, ?array $options = []): self + public function addSearchBox(?array $fields = [], ?array $options = []): self { $this->searchBoxFields = $fields; From c8762d2bc2a617a90af61bec89c1539906dca707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 13:10:28 +0200 Subject: [PATCH 2/6] list referral for an accompanying period --- .../Repository/UserRepository.php | 18 +++++++ .../Suggestion/ReferralAvailable.php | 31 ++++++++++++ .../Suggestion/ReferralAvailableInterface.php | 20 ++++++++ .../AccompanyingCourseApiController.php | 40 ++++++++++++++-- .../AccompanyingCourseApiControllerTest.php | 13 +++++ .../ChillPersonBundle/chill.api.specs.yaml | 48 +++++++++++++------ .../ChillPersonBundle/chill.webpack.config.js | 2 +- .../config/services/accompanyingPeriod.yaml | 7 +++ .../config/services/controller.yaml | 6 +-- 9 files changed, 161 insertions(+), 24 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php create mode 100644 src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailableInterface.php diff --git a/src/Bundle/ChillMainBundle/Repository/UserRepository.php b/src/Bundle/ChillMainBundle/Repository/UserRepository.php index e5427325a..825a1600e 100644 --- a/src/Bundle/ChillMainBundle/Repository/UserRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/UserRepository.php @@ -48,6 +48,24 @@ final class UserRepository implements ObjectRepository return $this->repository->findBy($criteria, $orderBy, $limit, $offset); } + public function countBy(array $criteria): int + { + return $this->repository->count($criteria); + } + + public function countByActive(): int + { + return $this->countBy(['enabled' => true]); + } + + /** + * @return User[]|array + */ + public function findByActive(array $orderBy = null, int $limit = null, int $offset = null): array + { + return $this->findBy(['enabled' => true], $orderBy, $limit, $offset); + } + public function getClassName() { return User::class; } diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php new file mode 100644 index 000000000..d70342610 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php @@ -0,0 +1,31 @@ +userRepository = $userRepository; + } + + public function countReferralAvailable(AccompanyingPeriod $period, ?array $options = []): int + { + return $this->userRepository->countByActive(); + } + + /** + * @param AccompanyingPeriod $period + * @return array|User[] + */ + public function findReferralAvailable(AccompanyingPeriod $period, int $limit = 50, int $start = 0): array + { + return $this->userRepository->findByActive(); + } +} diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailableInterface.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailableInterface.php new file mode 100644 index 000000000..f064fff99 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailableInterface.php @@ -0,0 +1,20 @@ +eventDispatcher = $eventDispatcher; $this->validator = $validator; $this->registry = $registry; + $this->referralAvailable = $referralAvailable; } public function confirmApi($id, Request $request, $_format): Response { - /** @var AccompanyingPeriod $accompanyingPeriod */ + /** @var AccompanyingPeriod $accompanyingPeriod */ $accompanyingPeriod = $this->getEntity('participation', $id, $request); $this->checkACL('confirm', $request, $_format, $accompanyingPeriod); @@ -58,10 +68,10 @@ $workflow = $this->registry->get($accompanyingPeriod); 'groups' => [ 'read' ] ]); } - + public function participationApi($id, Request $request, $_format) { - /** @var AccompanyingPeriod $accompanyingPeriod */ + /** @var AccompanyingPeriod $accompanyingPeriod */ $accompanyingPeriod = $this->getEntity('participation', $id, $request); $person = $this->getSerializer() ->deserialize($request->getContent(), Person::class, $_format, []); @@ -152,7 +162,7 @@ $workflow = $this->registry->get($accompanyingPeriod); ->deserialize($request->getContent(), $class, $_format, []); } catch (RuntimeException $e) { $exceptions[] = $e; - } + } } if ($requestor === null) { throw new BadRequestException('Could not find any person or requestor', 0, $exceptions[0]); @@ -187,4 +197,24 @@ $workflow = $this->registry->get($accompanyingPeriod); return null; } + + /** + * @Route("/api/1.0/person/accompanying-course/{id}/referral-availables.{_format}", + * requirements={ "_format"="json"}, + * name="chill_api_person_accompanying_period_referral_available") + * @param AccompanyingPeriod $period + * @return JsonResponse + */ + public function userReferral(AccompanyingPeriod $period, string $_format = 'json'): JsonResponse + { + $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $period); + + $total = $this->referralAvailable->countReferralAvailable($period); + $paginator = $this->getPaginatorFactory()->create($total); + $users = $this->referralAvailable->findReferralAvailable($period, $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber()); + + return $this->json(new Collection($users, $paginator), Response::HTTP_OK, + [], [ AbstractNormalizer::GROUPS => [ 'read' ]]); + } } diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php index a24e94bfa..80bb1d7cc 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php @@ -325,6 +325,19 @@ class AccompanyingCourseApiControllerTest extends WebTestCase $this->period = $period; } + /** + * @dataProvider dataGenerateRandomAccompanyingCourse + */ + public function testReferralAvailable(int $personId, int $periodId) + { + $this->client->request( + Request::METHOD_POST, + sprintf('/api/1.0/person/accompanying-course/%d/referral-availables.json', $periodId) + ); + + $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); + } + /** * * @dataProvider dataGenerateRandomAccompanyingCourse diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index 4041c52b7..a3621dac1 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -445,7 +445,7 @@ paths: /1.0/person/accompanying-course/{id}.json: get: tags: - - person + - accompanying-course summary: "Return the description for an accompanying course (accompanying period)" parameters: - name: id @@ -532,7 +532,7 @@ paths: /1.0/person/accompanying-course/{id}/requestor.json: post: tags: - - person + - accompanying-course summary: "Add a requestor to the accompanying course" parameters: - name: id @@ -574,7 +574,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the requestor for the accompanying course" parameters: - name: id @@ -598,7 +598,7 @@ paths: /1.0/person/accompanying-course/{id}/participation.json: post: tags: - - person + - accompanying-course summary: "Add a participant to the accompanying course" parameters: - name: id @@ -627,7 +627,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the participant for the accompanying course" parameters: - name: id @@ -658,7 +658,7 @@ paths: /1.0/person/accompanying-course/{id}/resource.json: post: tags: - - person + - accompanying-course summary: "Add a resource to the accompanying course" parameters: - name: id @@ -703,7 +703,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the resource" parameters: - name: id @@ -734,7 +734,7 @@ paths: /1.0/person/accompanying-course/{id}/comment.json: post: tags: - - person + - accompanying-course summary: "Add a comment to the accompanying course" parameters: - name: id @@ -772,7 +772,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the comment" parameters: - name: id @@ -803,7 +803,7 @@ paths: /1.0/person/accompanying-course/{id}/scope.json: post: tags: - - person + - accompanying-course summary: "Add a scope to the accompanying course" parameters: - name: id @@ -837,7 +837,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the scope" parameters: - name: id @@ -868,7 +868,7 @@ paths: /1.0/person/accompanying-course/{id}/socialissue.json: post: tags: - - person + - accompanying-course summary: "Add a social issue to the accompanying course" parameters: - name: id @@ -902,7 +902,7 @@ paths: description: "object with validation errors" delete: tags: - - person + - accompanying-course summary: "Remove the social issue" parameters: - name: id @@ -929,11 +929,31 @@ paths: description: "OK" 422: description: "object with validation errors" + /1.0/person/accompanying-course/{id}/referral-availables.json: + get: + tags: + - accompanying-course + summary: "get a list of available referral for a given accompanying cours" + parameters: + - name: id + in: path + required: true + description: The accompanying period's id + schema: + type: integer + format: integer + minimum: 1 + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" /1.0/person/accompanying-course/{id}/work.json: post: tags: - - person - accompanying-course-work summary: "Add a work (AccompanyingPeriodwork) to the accompanying course" parameters: diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 925a5ccf8..e8194b648 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -16,5 +16,5 @@ module.exports = function(encore, entries) encore.addEntry('page_household_edit_metadata', __dirname + '/Resources/public/page/household_edit_metadata/index.js'); encore.addEntry('page_person', __dirname + '/Resources/public/page/person/index.js'); encore.addEntry('page_accompanying_course_index_person_locate', __dirname + '/Resources/public/page/accompanying_course_index/person_locate.js'); - encore.addEntry('page_vis', __dirname + '/Resources/public/page/vis/index.js'); + //encore.addEntry('page_vis', __dirname + '/Resources/public/page/vis/index.js'); }; diff --git a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml index 88e70c9a8..5b8639146 100644 --- a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml @@ -13,3 +13,10 @@ services: entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod' lazy: true method: preUpdateAccompanyingPeriod + + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailable: + autowire: true + autoconfigure: true + + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailableInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailable' + diff --git a/src/Bundle/ChillPersonBundle/config/services/controller.yaml b/src/Bundle/ChillPersonBundle/config/services/controller.yaml index 489168425..098313286 100644 --- a/src/Bundle/ChillPersonBundle/config/services/controller.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/controller.yaml @@ -41,10 +41,8 @@ services: tags: ['controller.service_arguments'] Chill\PersonBundle\Controller\AccompanyingCourseApiController: - arguments: - $eventDispatcher: '@Symfony\Contracts\EventDispatcher\EventDispatcherInterface' - $validator: '@Symfony\Component\Validator\Validator\ValidatorInterface' - $registry: '@Symfony\Component\Workflow\Registry' + autoconfigure: true + autowire: true tags: ['controller.service_arguments'] Chill\PersonBundle\Controller\PersonApiController: From 0a058bad82e57f9a3deee2a61fdd3e71398e97a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 15:03:42 +0200 Subject: [PATCH 3/6] update list of referrers when loading and updating some parts of the course --- .../AccompanyingCourseApiController.php | 9 +++- .../public/vuejs/AccompanyingCourse/api.js | 11 ++++- .../components/Referrer.vue | 17 +------- .../vuejs/AccompanyingCourse/store/index.js | 43 ++++++++++++++++--- 4 files changed, 55 insertions(+), 25 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index ec6e5ced9..ebb36ae39 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -211,8 +211,13 @@ $workflow = $this->registry->get($accompanyingPeriod); $total = $this->referralAvailable->countReferralAvailable($period); $paginator = $this->getPaginatorFactory()->create($total); - $users = $this->referralAvailable->findReferralAvailable($period, $paginator->getItemsPerPage(), - $paginator->getCurrentPageFirstItemNumber()); + + if (0 < $total) { + $users = $this->referralAvailable->findReferralAvailable($period, $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber()); + } else { + $users = []; + } return $this->json(new Collection($users, $paginator), Response::HTTP_OK, [], [ AbstractNormalizer::GROUPS => [ 'read' ]]); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js index ee4c3e11b..c3b554dc4 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js @@ -1,3 +1,5 @@ +import { fetchResults } from 'ChillMainAssets/lib/api/download.js'; + /* * Endpoint v.2 chill_api_single_accompanying_course__entity * method GET/HEAD, get AccompanyingCourse Instance @@ -216,8 +218,6 @@ const addScope = (id, scope) => { const removeScope = (id, scope) => { const url = `/api/1.0/person/accompanying-course/${id}/scope.json`; - console.log(url); - console.log(scope); return fetch(url, { method: 'DELETE', @@ -235,6 +235,12 @@ const removeScope = (id, scope) => { }); }; +const getAvailableReferrals = (course) => { + const url = `/api/1.0/person/accompanying-course/${course.id}/referral-availables.json`; + + return fetchResults(url); +} + export { getAccompanyingCourse, patchAccompanyingCourse, @@ -249,4 +255,5 @@ export { postSocialIssue, addScope, removeScope, + getAvailableReferrals }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue index d1ecfbed1..169d8c3bb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue @@ -15,7 +15,7 @@ v-bind:searchable="true" v-bind:placeholder="$t('referrer.placeholder')" v-model="value" - v-bind:options="options" + v-bind:options="referrersAvailable" @select="updateReferrer"> @@ -45,26 +45,13 @@ import { mapState } from 'vuex'; export default { name: "Referrer", components: { VueMultiselect }, - data() { - return { - options: [] - } - }, computed: { ...mapState({ value: state => state.accompanyingCourse.user, + referrersAvailable: state => state.referrersAvailable, }), }, - mounted() { - this.getOptions(); - }, methods: { - getOptions() { - getUsers().then(response => new Promise((resolve, reject) => { - this.options = response.results; - resolve(); - })); - }, updateReferrer(value) { //console.log('value', value); this.$store.dispatch('updateReferrer', value); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js index ca1a89aa9..e944aa496 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js @@ -10,6 +10,7 @@ import { getAccompanyingCourse, postSocialIssue, addScope, removeScope, + getAvailableReferrals, } from '../api'; import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly"; import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly"; @@ -38,6 +39,8 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) scopesAtStart: accompanyingCourse.scopes.map(scope => scope), // the scope states at server side scopesAtBackend: accompanyingCourse.scopes.map(scope => scope), + // the users which are available for referrer + referrersAvailable: [], }, getters: { isParticipationValid(state) { @@ -170,6 +173,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) //console.log('value', value); state.accompanyingCourse.user = value; }, + setReferrersAvailables(state, users) { + state.referrersAvailable = users; + }, confirmAccompanyingCourse(state, response) { //console.log('### mutation: confirmAccompanyingCourse: response', response); state.accompanyingCourse.step = response.step; @@ -272,6 +278,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) })).catch((error) => { commit('catchError', error) }); }, patchOnTheFly({ commit }, payload) { + // TODO should be into the dedicated component, no ? JF console.log('## action: patch OnTheFly', payload); let body = { type: payload.type }; if (payload.type === 'person') { @@ -313,10 +320,12 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) }, toggleEmergency({ commit }, payload) { patchAccompanyingCourse(id, { type: "accompanying_period", emergency: payload }) - .then(course => new Promise((resolve, reject) => { - commit('toggleEmergency', course.emergency); - resolve(); - })).catch((error) => { commit('catchError', error) }); + .then(course => new Promise((resolve, reject) => { + commit('toggleEmergency', course.emergency); + return dispatch('setRefererresAvailable'); + })) + .then(() => Promise.resolve()) + .catch((error) => { commit('catchError', error) }); }, toggleConfidential({ commit }, payload) { patchAccompanyingCourse(id, { type: "accompanying_period", confidential: payload }) @@ -387,11 +396,15 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) // check/uncheck in the UI. I do not know of to avoid it. commit('setScopes', scopes); return Promise.resolve(); + }).then(() => { + return dispatch('fetchReferrersAvailable'); }); } else { return dispatch('setScopes', state.scopesAtStart).then(() => { commit('setScopes', scopes); return Promise.resolve(); + }).then(() => { + return dispatch('fetchReferrersAvailable'); }); } }, @@ -434,7 +447,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) resolve(); })).catch((error) => { commit('catchError', error) }); }, - updateSocialIssues({ state, commit }, { payload, body, method }) { + updateSocialIssues({ state, commit, dispatch }, { payload, body, method }) { //console.log('## action: payload', { payload, body, method }); postSocialIssue(id, body, method) .then(response => new Promise((resolve, reject) => { @@ -445,6 +458,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) return getAccompanyingCourse(state.accompanyingCourse.id); }).then(accompanying_course => { commit('refreshSocialIssues', accompanying_course.socialIssues); + dispatch('fetchReferrersAvailable'); }) .catch((error) => { commit('catchError', error) }); }, @@ -462,7 +476,21 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) resolve(); })).catch((error) => { commit('catchError', error) }); }, - updateLocation({ commit }, payload) { + async fetchReferrersAvailable({ state, commit}) { + let users = await getAvailableReferrals(state.accompanyingCourse); + commit('setReferrersAvailables', users); + let userIds = users.map(u => u.id); + if ( + null !== state.accompanyingCourse.user + && !state.accompanyingCourse.confidential + && !state.accompanyingCourse.step === 'DRAFT' + ) { + if (!userIds.include(state.accompanyingCourse.user.id)) { + commit('updateReferrer', null); + } + } + }, + updateLocation({ commit, dispatch }, payload) { //console.log('## action: updateLocation', payload.locationStatusTo); let body = { 'type': payload.target, 'id': payload.targetId }; let location = {}; @@ -484,6 +512,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) personLocation: accompanyingCourse.personLocation }); resolve(); + dispatch('fetchReferrersAvailable'); })).catch((error) => { commit('catchError', error) }); }, confirmAccompanyingCourse({ commit }) { @@ -498,6 +527,8 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) } } }); + + store.dispatch('fetchReferrersAvailable'); resolve(store); })); From 3f138dc1522d8ac2e27a740fa8a800d95c6f43eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 19:59:08 +0200 Subject: [PATCH 4/6] show users as suggestions, not in constrained list --- .../ChillMainBundle/ChillMainBundle.php | 3 + src/Bundle/ChillMainBundle/Entity/UserJob.php | 6 ++ .../_components/Entity/UserRenderBoxBadge.vue | 14 ++++ .../Resources/views/Entity/user.html.twig | 9 +++ .../Serializer/Normalizer/UserNormalizer.php | 23 +++++-- .../Templating/Entity/UserRender.php | 66 +++++++++++++++++++ .../config/services/templating.yaml | 10 +-- .../AccompanyingCourseApiController.php | 4 +- .../public/vuejs/AccompanyingCourse/api.js | 13 ++-- .../components/Referrer.vue | 29 +++++++- .../vuejs/AccompanyingCourse/store/index.js | 58 +++++++++++----- .../AccompanyingCourseApiControllerTest.php | 2 +- .../ChillPersonBundle/chill.api.specs.yaml | 2 +- 13 files changed, 197 insertions(+), 42 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig create mode 100644 src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index 91c58f68b..f9e79bc8b 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -8,6 +8,7 @@ use Chill\MainBundle\Security\Authorization\ChillVoterInterface; use Chill\MainBundle\Security\ProvideRoleInterface; use Chill\MainBundle\Security\Resolver\CenterResolverInterface; use Chill\MainBundle\Security\Resolver\ScopeResolverInterface; +use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Chill\MainBundle\DependencyInjection\CompilerPass\SearchableServicesCompilerPass; @@ -38,6 +39,8 @@ class ChillMainBundle extends Bundle ->addTag('chill_main.center_resolver'); $container->registerForAutoconfiguration(ScopeResolverInterface::class) ->addTag('chill_main.scope_resolver'); + $container->registerForAutoconfiguration(ChillEntityRenderInterface::class) + ->addTag('chill.render_entity'); $container->addCompilerPass(new SearchableServicesCompilerPass()); $container->addCompilerPass(new ConfigConsistencyCompilerPass()); diff --git a/src/Bundle/ChillMainBundle/Entity/UserJob.php b/src/Bundle/ChillMainBundle/Entity/UserJob.php index 9d1ca9157..411d5c40f 100644 --- a/src/Bundle/ChillMainBundle/Entity/UserJob.php +++ b/src/Bundle/ChillMainBundle/Entity/UserJob.php @@ -3,10 +3,14 @@ namespace Chill\MainBundle\Entity; use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Serializer\Annotation as Serializer; /** * @ORM\Entity * @ORM\Table("chill_main_user_job") + * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ + * "user_job"=UserJob::class + * }) */ class UserJob { @@ -15,12 +19,14 @@ class UserJob * @ORM\Id * @ORM\Column(name="id", type="integer") * @ORM\GeneratedValue(strategy="AUTO") + * @Serializer\Groups({"read"}) */ protected ?int $id = null; /** * @var array|string[]A * @ORM\Column(name="label", type="json") + * @Serializer\Groups({"read"}) */ protected array $label = []; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue new file mode 100644 index 000000000..a5e1cf183 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Entity/UserRenderBoxBadge.vue @@ -0,0 +1,14 @@ + + + diff --git a/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig new file mode 100644 index 000000000..77dc959a2 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Entity/user.html.twig @@ -0,0 +1,9 @@ + + {{- user.label }} + {%- if opts['user_job'] and user.userJob is not null %} + ({{ user.userJob.label|localize_translatable_string }}) + {%- endif -%} + {%- if opts['main_scope'] and user.mainScope is not null %} + ({{ user.mainScope.name|localize_translatable_string }}) + {%- endif -%} + diff --git a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php index 4c1dc1523..af29ad4f4 100644 --- a/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php +++ b/src/Bundle/ChillMainBundle/Serializer/Normalizer/UserNormalizer.php @@ -20,14 +20,21 @@ namespace Chill\MainBundle\Serializer\Normalizer; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Templating\Entity\UserRender; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; +use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -/** - * - * @internal we keep this normalizer, because the property 'text' may be replace by a rendering in the future - */ -class UserNormalizer implements NormalizerInterface +class UserNormalizer implements NormalizerInterface, NormalizerAwareInterface { + use NormalizerAwareTrait; + private UserRender $userRender; + + public function __construct(UserRender $userRender) + { + $this->userRender = $userRender; + } + public function normalize($user, string $format = null, array $context = array()) { /** @var User $user */ @@ -35,7 +42,11 @@ class UserNormalizer implements NormalizerInterface 'type' => 'user', 'id' => $user->getId(), 'username' => $user->getUsername(), - 'text' => $user->getUsername() + 'text' => $this->userRender->renderString($user, []), + 'label' => $user->getLabel(), + 'user_job' => $this->normalizer->normalize($user->getUserJob(), $format, $context), + 'main_center' => $this->normalizer->normalize($user->getMainCenter(), $format, $context), + 'main_scope' => $this->normalizer->normalize($user->getMainScope(), $format, $context), ]; } diff --git a/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php new file mode 100644 index 000000000..121b94ead --- /dev/null +++ b/src/Bundle/ChillMainBundle/Templating/Entity/UserRender.php @@ -0,0 +1,66 @@ + true, + 'user_job' => true + ]; + + public function __construct(TranslatableStringHelper $translatableStringHelper, EngineInterface $engine) + { + $this->translatableStringHelper = $translatableStringHelper; + $this->engine = $engine; + } + + /** + * @inheritDoc + */ + public function supports($entity, array $options): bool + { + return $entity instanceof User; + } + + /** + * @inheritDoc + * @param User $entity + */ + public function renderString($entity, array $options): string + { + $opts = \array_merge(self::DEFAULT_OPTIONS, $options); + + $str = $entity->getLabel(); + if (NULL !== $entity->getUserJob() && $opts['user_job']) { + $str .= ' ('.$this->translatableStringHelper + ->localize($entity->getUserJob()->getLabel()).')'; + } + if (NULL !== $entity->getMainScope() && $opts['main_scope']) { + $str .= ' ('.$this->translatableStringHelper + ->localize($entity->getMainScope()->getName()).')'; + } + + return $str; + } + + /** + * @inheritDoc + */ + public function renderBox($entity, array $options): string + { + $opts = \array_merge(self::DEFAULT_OPTIONS, $options); + + return $this->engine->render('@ChillMain/Entity/user.html.twig', [ + 'user' => $entity, + 'opts' => $opts + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/templating.yaml b/src/Bundle/ChillMainBundle/config/services/templating.yaml index e264f3a99..02d806db4 100644 --- a/src/Bundle/ChillMainBundle/config/services/templating.yaml +++ b/src/Bundle/ChillMainBundle/config/services/templating.yaml @@ -42,10 +42,12 @@ services: - { name: twig.extension } Chill\MainBundle\Templating\Entity\AddressRender: - arguments: - - '@Symfony\Component\Templating\EngineInterface' - tags: - - { name: 'chill.render_entity' } + autoconfigure: true + autowire: true + + Chill\MainBundle\Templating\Entity\UserRender: + autoconfigure: true + autowire: true Chill\MainBundle\Templating\Listing\: resource: './../../Templating/Listing' diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index ebb36ae39..428d2a15f 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -199,9 +199,9 @@ $workflow = $this->registry->get($accompanyingPeriod); } /** - * @Route("/api/1.0/person/accompanying-course/{id}/referral-availables.{_format}", + * @Route("/api/1.0/person/accompanying-course/{id}/referrers-suggested.{_format}", * requirements={ "_format"="json"}, - * name="chill_api_person_accompanying_period_referral_available") + * name="chill_api_person_accompanying_period_referrers_suggested") * @param AccompanyingPeriod $period * @return JsonResponse */ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js index c3b554dc4..52f3f6c36 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js @@ -170,11 +170,8 @@ const postSocialIssue = (id, body, method) => { const getUsers = () => { const url = `/api/1.0/main/user.json`; - return fetch(url) - .then(response => { - if (response.ok) { return response.json(); } - throw { msg: 'Error while retriving users.', sta: response.status, txt: response.statusText, err: new Error(), body: response.body }; - }); + + return fetchResults(url); }; const whoami = () => { @@ -235,8 +232,8 @@ const removeScope = (id, scope) => { }); }; -const getAvailableReferrals = (course) => { - const url = `/api/1.0/person/accompanying-course/${course.id}/referral-availables.json`; +const getReferrersSuggested = (course) => { + const url = `/api/1.0/person/accompanying-course/${course.id}/referrers-suggested.json`; return fetchResults(url); } @@ -255,5 +252,5 @@ export { postSocialIssue, addScope, removeScope, - getAvailableReferrals + getReferrersSuggested, }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue index 169d8c3bb..c39e043c6 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue @@ -15,9 +15,20 @@ v-bind:searchable="true" v-bind:placeholder="$t('referrer.placeholder')" v-model="value" - v-bind:options="referrersAvailable" + v-bind:options="users" @select="updateReferrer"> + + + + +
@@ -41,14 +52,26 @@ import VueMultiselect from 'vue-multiselect'; import { getUsers, whoami } from '../api'; import { mapState } from 'vuex'; +import UserRenderBoxBadge from "ChillMainAssets/vuejs/_components/Entity/UserRenderBoxBadge"; export default { name: "Referrer", - components: { VueMultiselect }, + components: { + UserRenderBoxBadge, + VueMultiselect, + }, computed: { ...mapState({ value: state => state.accompanyingCourse.user, - referrersAvailable: state => state.referrersAvailable, + users: state => state.users, + referrersSuggested: state => { + return state.referrersSuggested.filter(u => { + if (null === state.accompanyingCourse.user) { + return true; + } + return state.accompanyingCourse.user.id !== u.id; + }) + }, }), }, methods: { diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js index e944aa496..9b69845cf 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/store/index.js @@ -10,7 +10,8 @@ import { getAccompanyingCourse, postSocialIssue, addScope, removeScope, - getAvailableReferrals, + getReferrersSuggested, + getUsers, } from '../api'; import { patchPerson } from "ChillPersonAssets/vuejs/_api/OnTheFly"; import { patchThirdparty } from "ChillThirdPartyAssets/vuejs/_api/OnTheFly"; @@ -40,7 +41,9 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) // the scope states at server side scopesAtBackend: accompanyingCourse.scopes.map(scope => scope), // the users which are available for referrer - referrersAvailable: [], + referrersSuggested: [], + // all the users available + users: [], }, getters: { isParticipationValid(state) { @@ -173,8 +176,15 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) //console.log('value', value); state.accompanyingCourse.user = value; }, - setReferrersAvailables(state, users) { - state.referrersAvailable = users; + setReferrersSuggested(state, users) { + state.referrersSuggested = users.map(u => { + if (state.accompanyingCourse.user !== null) { + if (state.accompanyingCourse.user.id === u.id) { + return state.accompanyingCourse.user; + } + } + return u; + }); }, confirmAccompanyingCourse(state, response) { //console.log('### mutation: confirmAccompanyingCourse: response', response); @@ -184,6 +194,16 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) //console.log('define location context'); state.addressContext = context; }, + setUsers(state, users) { + state.users = users.map(u => { + if (state.accompanyingCourse.user !== null) { + if (state.accompanyingCourse.user.id === u.id) { + return state.accompanyingCourse.user; + } + } + return u; + }); + }, updateLocation(state, r) { //console.log('### mutation: set location attributes', r); state.accompanyingCourse.locationStatus = r.locationStatus; @@ -397,14 +417,14 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) commit('setScopes', scopes); return Promise.resolve(); }).then(() => { - return dispatch('fetchReferrersAvailable'); + return dispatch('fetchReferrersSuggested'); }); } else { return dispatch('setScopes', state.scopesAtStart).then(() => { commit('setScopes', scopes); return Promise.resolve(); }).then(() => { - return dispatch('fetchReferrersAvailable'); + return dispatch('fetchReferrersSuggested'); }); } }, @@ -458,7 +478,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) return getAccompanyingCourse(state.accompanyingCourse.id); }).then(accompanying_course => { commit('refreshSocialIssues', accompanying_course.socialIssues); - dispatch('fetchReferrersAvailable'); + dispatch('fetchReferrersSuggested'); }) .catch((error) => { commit('catchError', error) }); }, @@ -476,20 +496,23 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) resolve(); })).catch((error) => { commit('catchError', error) }); }, - async fetchReferrersAvailable({ state, commit}) { - let users = await getAvailableReferrals(state.accompanyingCourse); - commit('setReferrersAvailables', users); - let userIds = users.map(u => u.id); + async fetchReferrersSuggested({ state, commit}) { + let users = await getReferrersSuggested(state.accompanyingCourse); + commit('setReferrersSuggested', users); if ( - null !== state.accompanyingCourse.user + null === state.accompanyingCourse.user && !state.accompanyingCourse.confidential && !state.accompanyingCourse.step === 'DRAFT' + && users.length === 1 ) { - if (!userIds.include(state.accompanyingCourse.user.id)) { - commit('updateReferrer', null); - } + // set the user if unique + commit('updateReferrer', users[0]); } }, + async fetchUsers({commit}) { + let users = await getUsers(); + commit('setUsers', users); + }, updateLocation({ commit, dispatch }, payload) { //console.log('## action: updateLocation', payload.locationStatusTo); let body = { 'type': payload.target, 'id': payload.targetId }; @@ -512,7 +535,7 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) personLocation: accompanyingCourse.personLocation }); resolve(); - dispatch('fetchReferrersAvailable'); + dispatch('fetchReferrersSuggested'); })).catch((error) => { commit('catchError', error) }); }, confirmAccompanyingCourse({ commit }) { @@ -528,7 +551,8 @@ let initPromise = Promise.all([scopesPromise, accompanyingCoursePromise]) } }); - store.dispatch('fetchReferrersAvailable'); + store.dispatch('fetchReferrersSuggested'); + store.dispatch('fetchUsers'); resolve(store); })); diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php index 80bb1d7cc..78376b8bd 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/AccompanyingCourseApiControllerTest.php @@ -332,7 +332,7 @@ class AccompanyingCourseApiControllerTest extends WebTestCase { $this->client->request( Request::METHOD_POST, - sprintf('/api/1.0/person/accompanying-course/%d/referral-availables.json', $periodId) + sprintf('/api/1.0/person/accompanying-course/%d/referrers-suggested.json', $periodId) ); $this->assertEquals(200, $this->client->getResponse()->getStatusCode()); diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index a3621dac1..a87711de2 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -929,7 +929,7 @@ paths: description: "OK" 422: description: "object with validation errors" - /1.0/person/accompanying-course/{id}/referral-availables.json: + /1.0/person/accompanying-course/{id}/referrers-suggested.json: get: tags: - accompanying-course From 02ca9add522d794a67cb0482c326a67917e45f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 20:49:44 +0200 Subject: [PATCH 5/6] refactor naming ReferralsAvailable => referralSuggestion --- .../Suggestion/ReferralAvailable.php | 31 ------------------- .../Suggestion/ReferralsSuggestion.php | 26 ++++++++++++++++ ...e.php => ReferralsSuggestionInterface.php} | 6 ++-- .../AccompanyingCourseApiController.php | 10 +++--- .../config/services/accompanyingPeriod.yaml | 4 +-- 5 files changed, 36 insertions(+), 41 deletions(-) delete mode 100644 src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php create mode 100644 src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralsSuggestion.php rename src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/{ReferralAvailableInterface.php => ReferralsSuggestionInterface.php} (68%) diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php deleted file mode 100644 index d70342610..000000000 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralAvailable.php +++ /dev/null @@ -1,31 +0,0 @@ -userRepository = $userRepository; - } - - public function countReferralAvailable(AccompanyingPeriod $period, ?array $options = []): int - { - return $this->userRepository->countByActive(); - } - - /** - * @param AccompanyingPeriod $period - * @return array|User[] - */ - public function findReferralAvailable(AccompanyingPeriod $period, int $limit = 50, int $start = 0): array - { - return $this->userRepository->findByActive(); - } -} diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralsSuggestion.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralsSuggestion.php new file mode 100644 index 000000000..b28dcce79 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Suggestion/ReferralsSuggestion.php @@ -0,0 +1,26 @@ +eventDispatcher = $eventDispatcher; $this->validator = $validator; @@ -209,11 +209,11 @@ $workflow = $this->registry->get($accompanyingPeriod); { $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $period); - $total = $this->referralAvailable->countReferralAvailable($period); + $total = $this->referralAvailable->countReferralSuggested($period); $paginator = $this->getPaginatorFactory()->create($total); if (0 < $total) { - $users = $this->referralAvailable->findReferralAvailable($period, $paginator->getItemsPerPage(), + $users = $this->referralAvailable->findReferralSuggested($period, $paginator->getItemsPerPage(), $paginator->getCurrentPageFirstItemNumber()); } else { $users = []; diff --git a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml index 5b8639146..d9e784239 100644 --- a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml @@ -14,9 +14,9 @@ services: lazy: true method: preUpdateAccompanyingPeriod - Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailable: + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion: autowire: true autoconfigure: true - Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailableInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralAvailable' + Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion' From dded4fb804c0b9af51b82da54b3d8764f73175a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 20 Oct 2021 20:50:27 +0200 Subject: [PATCH 6/6] rename method --- .../Controller/AccompanyingCourseApiController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php index 306a0c88b..bce888876 100644 --- a/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php +++ b/src/Bundle/ChillPersonBundle/Controller/AccompanyingCourseApiController.php @@ -205,7 +205,7 @@ $workflow = $this->registry->get($accompanyingPeriod); * @param AccompanyingPeriod $period * @return JsonResponse */ - public function userReferral(AccompanyingPeriod $period, string $_format = 'json'): JsonResponse + public function suggestReferrals(AccompanyingPeriod $period, string $_format = 'json'): JsonResponse { $this->denyAccessUnlessGranted(AccompanyingPeriodVoter::EDIT, $period);