From c6949490a4fb46c74e8d6ae8b53c0fd9631a7635 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 2 Jun 2021 00:32:55 +0200 Subject: [PATCH 01/15] first impl for household editor --- .../Controller/HouseholdMemberController.php | 53 +++++++- .../Entity/Household/Position.php | 6 +- .../Household/PositionRepository.php | 21 +++- .../vuejs/HouseholdMembersEditor/App.vue | 23 ++++ .../components/Concerned.vue | 118 ++++++++++++++++++ .../vuejs/HouseholdMembersEditor/index.js | 16 +++ .../vuejs/HouseholdMembersEditor/js/i18n.js | 20 +++ .../HouseholdMembersEditor/store/index.js | 74 +++++++++++ .../views/Household/members_editor.html.twig | 17 +++ .../ChillPersonBundle/chill.webpack.config.js | 1 + 10 files changed, 340 insertions(+), 9 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php index c6fd4dd59..e55fb3e9f 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -2,6 +2,9 @@ namespace Chill\PersonBundle\Controller; +use Chill\PersonBundle\Entity\Household\Position; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\PersonBundle\Entity\Person; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -15,7 +18,7 @@ class HouseholdMemberController extends ApiController /** * @Route( * "/api/1.0/person/household/members/move.{_format}", - * name="chill_person_household_members_move" + * name="chill_api_person_household_members_move" * ) */ public function move(Request $request, $_format): Response @@ -27,7 +30,6 @@ class HouseholdMemberController extends ApiController } catch (Exception\InvalidArgumentException | Exception\UnexpectedValueException $e) { throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e); } - dump($editor); // TODO ACL // // TODO validation @@ -44,7 +46,52 @@ class HouseholdMemberController extends ApiController $em->flush(); $em->commit(); - return $this->json($editor->getHousehold(), Response::HTTP_OK, ["groups" => ["read"]]); } + + /** + * @Route( + * "/{_locale}/person/household/members/editor", + * name="chill_person_household_members_editor" + * ) + */ + public function editor(Request $request) + { + if ($request->query->has('persons')) { + $ids = $request->query->get('persons', []); + + if (0 === count($ids)) { + throw new BadRequestExceptions("parameters persons in query ". + "is not an array or empty"); + } + + $persons = $this->getDoctrine()->getManager() + ->getRepository(Person::class) + ->findById($ids) + ; + + foreach ($persons as $person) { + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person, + "You are not allowed to see person with id {$person->getId()}" + ); + } + } + + $positions = $this->getDoctrine()->getManager() + ->getRepository(Position::class) + ->findAll() + ; + + $data = [ + 'persons' => $persons ?? false ? + $this->getSerializer()->normalize($persons, 'json', [ 'groups' => [ 'read' ]]): [], + 'household' => null, + 'positions' => + $this->getSerializer()->normalize($positions, 'json', [ 'groups' => [ 'read' ]]) + ]; + + return $this->render('@ChillPerson/Household/members_editor.html.twig', [ + 'data' => $data + ]); + } } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php index 156631b63..db8e32134 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Position.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Position.php @@ -7,7 +7,7 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; /** - * @ORM\Entity(repositoryClass=PositionRepository::class) + * @ORM\Entity * @ORM\Table(name="chill_person_household_position") * @Serializer\DiscriminatorMap(typeProperty="type", mapping={ * "household_position"=Position::class @@ -25,21 +25,25 @@ class Position /** * @ORM\Column(type="json") + * @Serializer\Groups({ "read" }) */ private $label = []; /** * @ORM\Column(type="boolean") + * @Serializer\Groups({ "read" }) */ private $shareHouseHold; /** * @ORM\Column(type="boolean") + * @Serializer\Groups({ "read" }) */ private $allowHolder; /** * @ORM\Column(type="float") + * @Serializer\Groups({ "read" }) */ private $ordering; diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php index 6d07f791e..a02de20dd 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Household/PositionRepository.php @@ -3,8 +3,10 @@ namespace Chill\PersonBundle\Repository\Household; use Chill\PersonBundle\Entity\Household\Position; -use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; -use Doctrine\Persistence\ManagerRegistry; +//use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; +//use Doctrine\Persistence\ManagerRegistry; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; /** * @method Position|null find($id, $lockMode = null, $lockVersion = null) @@ -12,11 +14,20 @@ use Doctrine\Persistence\ManagerRegistry; * @method Position[] findAll() * @method Position[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) */ -class PositionRepository extends ServiceEntityRepository +final class PositionRepository { - public function __construct(ManagerRegistry $registry) + private EntityRepository $repository; + + public function __construct(EntityManagerInterface $entityManager) { - parent::__construct($registry, Position::class); + $this->repository = $entityManager->getRepository(Position::class); } + /** + * @return Position[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue new file mode 100644 index 000000000..a658a06db --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue new file mode 100644 index 000000000..05e999350 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js new file mode 100644 index 000000000..c9f5bb111 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/index.js @@ -0,0 +1,16 @@ +import { createApp } from 'vue'; +import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'; +import { appMessages } from './js/i18n'; +import { store } from './store'; + +import App from './App.vue'; + +const i18n = _createI18n(appMessages); + +const app = createApp({ + template: ``, +}) +.use(store) +.use(i18n) +.component('app', App) +.mount('#household_members_editor'); diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js new file mode 100644 index 000000000..d297ba1d7 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js @@ -0,0 +1,20 @@ + +import { personMessages } from 'ChillPersonAssets/vuejs/_js/i18n' + +const appMessages = { + fr: { + household_members_editor: { + concerned: { + title: "Personnes concernées", + add_persons: "Ajouter d'autres usagers", + search: "Rechercher des usagers", + } + } + } +}; + +Object.assign(appMessages.fr, personMessages.fr); + +export { + appMessages +}; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js new file mode 100644 index 000000000..257a46e57 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -0,0 +1,74 @@ +import { createStore } from 'vuex'; + +const debug = process.env.NODE_ENV !== 'production'; + +const concerned = window.household_members_editor_data.persons.map(p => { + return { + person: p, + position: null, + start_date: null + }; +}); + +const store = createStore({ + strict: debug, + state: { + concerned, + household: window.household_members_editor_data.household, + positions: window.household_members_editor_data.positions, + }, + getters: { + persons(state) { + return state.concerned.map(conc => conc.person); + }, + personsUnpositionned(state) { + return state.concerned + .filter(conc => conc.position === null) + .map(conc => conc.person) + ; + }, + personByPosition: (state) => (position_id) => { + return state.concerned + .filter(conc => + conc.position !== null ? conc.position.id === position_id : false + ) + .map(conc => conc.person) + ; + }, + positions(state) { + return state.positions; + } + }, + mutations: { + addConcerned(state, person) { + console.log('from mutation addConcerned'); + state.concerned.push({ person, position: null, start_date: null }); + }, + markPosition(state, { person_id, position_id}) { + console.log('from mutation markPosition'); + console.log('person_id', person_id); + console.log('position_id', position_id); + console.log('state', state.concerned); + let + position = state.positions.find(pos => pos.id === position_id), + conc = state.concerned.find(c => c.person.id === person_id); + console.log(position); + console.log(conc); + conc.position = position; + } + }, + actions: { + addConcerned({ commit }, person) { + console.log('from actions addConcerned'); + commit('addConcerned', person); + }, + markPosition({ commit, state }, { person_id, position_id }) { + console.log('from action markPosition'); + console.log('person_id', person_id); + console.log('position_id', position_id); + commit('markPosition', { person_id, position_id }); + } + } +}); + +export { store }; diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig new file mode 100644 index 000000000..f0433a812 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig @@ -0,0 +1,17 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block content %} +

Editor

+
+{% endblock %} + +{% block js %} + + {{ encore_entry_script_tags('household_members_editor') }} +{% endblock %} + +{% block css %} + {{ encore_entry_link_tags('household_members_editor') }} +{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 53aed5d91..60ca5501a 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -9,4 +9,5 @@ module.exports = function(encore, entries) }); encore.addEntry('accompanying_course', __dirname + '/Resources/public/vuejs/AccompanyingCourse/index.js'); + encore.addEntry('household_members_editor', __dirname + '/Resources/public/vuejs/HouseholdMembersEditor/index.js'); }; From d9a3e117b26f056add6d47a49dd421b47d2c6db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 3 Jun 2021 18:11:05 +0200 Subject: [PATCH 02/15] WIP continue editor --- .../Controller/HouseholdMemberController.php | 30 +++++-- .../vuejs/HouseholdMembersEditor/App.vue | 6 +- .../components/Concerned.vue | 53 ++++++++---- .../components/Household.vue | 70 ++++++++++++++++ .../components/MemberDetails.vue | 51 +++++++++++ .../vuejs/HouseholdMembersEditor/js/i18n.js | 12 ++- .../HouseholdMembersEditor/store/index.js | 84 ++++++++++++++++--- 7 files changed, 274 insertions(+), 32 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php index e55fb3e9f..24d77467d 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -5,6 +5,7 @@ namespace Chill\PersonBundle\Controller; use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\Household\Household; use Symfony\Component\HttpFoundation\Exception\BadRequestException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -57,6 +58,8 @@ class HouseholdMemberController extends ApiController */ public function editor(Request $request) { + $em = $this->getDoctrine()->getManager(); + if ($request->query->has('persons')) { $ids = $request->query->get('persons', []); @@ -65,8 +68,7 @@ class HouseholdMemberController extends ApiController "is not an array or empty"); } - $persons = $this->getDoctrine()->getManager() - ->getRepository(Person::class) + $persons = $em->getRepository(Person::class) ->findById($ids) ; @@ -77,6 +79,20 @@ class HouseholdMemberController extends ApiController } } + if ($householdId = $request->query->get('household', false)) { + $household = $em->getRepository(Household::class) + ->find($householdId) + ; + $allowHouseholdCreate = false; + $allowHouseholdSearch = false; + $allowLeaveWithoutHousehold = false; + + if (NULL === $household) { + throw $this->createNotFoundException('household not found'); + } + // TODO ACL on household + } + $positions = $this->getDoctrine()->getManager() ->getRepository(Position::class) ->findAll() @@ -84,10 +100,14 @@ class HouseholdMemberController extends ApiController $data = [ 'persons' => $persons ?? false ? - $this->getSerializer()->normalize($persons, 'json', [ 'groups' => [ 'read' ]]): [], - 'household' => null, + $this->getSerializer()->normalize($persons, 'json', [ 'groups' => [ 'read' ]]) : [], + 'household' => $household ?? false ? + $this->getSerializer()->normalize($household, 'json', [ 'groups' => [ 'read' ]]) : null, 'positions' => - $this->getSerializer()->normalize($positions, 'json', [ 'groups' => [ 'read' ]]) + $this->getSerializer()->normalize($positions, 'json', [ 'groups' => [ 'read' ]]), + 'allowHouseholdCreate' => $allowHouseholdCreate ?? true, + 'allowHouseholdSearch' => $allowHouseholdSearch ?? true, + 'allowLeaveWithoutHousehold' => $allowLeaveWithoutHousehold ?? $request->query->has('allow_leave_without_household'), ]; return $this->render('@ChillPerson/Household/members_editor.html.twig', [ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue index a658a06db..e5d84e920 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -1,15 +1,19 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue new file mode 100644 index 000000000..81aced606 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Household.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue new file mode 100644 index 000000000..cfefa9654 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/MemberDetails.vue @@ -0,0 +1,51 @@ + + + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js index d297ba1d7..c7724daff 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js @@ -5,10 +5,18 @@ const appMessages = { fr: { household_members_editor: { concerned: { - title: "Personnes concernées", + title: "Usagers concernés", add_persons: "Ajouter d'autres usagers", search: "Rechercher des usagers", - } + move_to: "Déplacer vers", + }, + holder: "Titulaire du ménage", + remove_position: "Retirer des {position}", + remove_concerned: "Enlever du ménage", + household_part: "Ménage de destination", + new_household: "Nouveau ménage", + create_household: "Créer un ménage", + search_household: "Chercher un ménage", } } }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js index 257a46e57..f5db83c66 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -6,7 +6,8 @@ const concerned = window.household_members_editor_data.persons.map(p => { return { person: p, position: null, - start_date: null + start_date: null, + allowRemove: false, }; }); @@ -16,17 +17,31 @@ const store = createStore({ concerned, household: window.household_members_editor_data.household, positions: window.household_members_editor_data.positions, + allowHouseholdCreate: window.household_members_editor_data.allowHouseholdCreate, + allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, + allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, + forceLeaveWithoutHousehold: false }, getters: { + isHouseholdNew(state) { + console.log('isHouseholdNew', !Number.isInteger(state.household.id)); + console.log('household', state.household); + return !Number.isInteger(state.household.id); + }, + hasHousehold(state) { + return state.household !== null; + }, persons(state) { return state.concerned.map(conc => conc.person); }, - personsUnpositionned(state) { + concUnpositionned(state) { return state.concerned .filter(conc => conc.position === null) - .map(conc => conc.person) ; }, + positions(state) { + return state.positions; + }, personByPosition: (state) => (position_id) => { return state.concerned .filter(conc => @@ -35,14 +50,28 @@ const store = createStore({ .map(conc => conc.person) ; }, - positions(state) { - return state.positions; - } + concByPosition: (state) => (position_id) => { + return state.concerned + .filter(conc => + conc.position !== null ? conc.position.id === position_id : false + ) + ; + }, + concByPersonId: (state) => (person_id) => { + return state.concerned + .find(conc => conc.person.id === person_id) + ; + }, }, mutations: { addConcerned(state, person) { - console.log('from mutation addConcerned'); - state.concerned.push({ person, position: null, start_date: null }); + let persons = state.concerned.map(conc => conc.person.id); + if (!persons.includes(person.id)) { + state.concerned.push({ person, position: null, + start_date: null, allowRemove: true }); + } else { + console.err("person already included"); + } }, markPosition(state, { person_id, position_id}) { console.log('from mutation markPosition'); @@ -55,7 +84,27 @@ const store = createStore({ console.log(position); console.log(conc); conc.position = position; - } + }, + toggleHolder(state, conc) { + console.log('toggleHolder', conc); + conc.holder = !conc.holder; + }, + removePosition(state, conc) { + conc.holder = false; + conc.position = null; + }, + removeConcerned(state, conc) { + state.concerned = state.concerned.filter(c => + c.person.id !== conc.person.id + ) + }, + createHousehold(state) { + state.household = { type: 'household', members: [], address: null } + }, + forceLeaveWithoutHousehold(state) { + state.household = null; + state.forceLeaveWithoutHousehold = true; + }, }, actions: { addConcerned({ commit }, person) { @@ -67,7 +116,22 @@ const store = createStore({ console.log('person_id', person_id); console.log('position_id', position_id); commit('markPosition', { person_id, position_id }); - } + }, + toggleHolder({ commit }, conc) { + commit('toggleHolder', conc); + }, + removePosition({ commit }, conc) { + commit('removePosition', conc); + }, + removeConcerned({ commit }, conc) { + commit('removeConcerned', conc); + }, + createHousehold({ commit }) { + commit('createHousehold'); + }, + forceLeaveWithoutHousehold({ commit }) { + commit('forceLeaveWithoutHousehold'); + }, } }); From 502a629dc86dddc5513bbe0f931972b3efa02e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 3 Jun 2021 22:09:52 +0200 Subject: [PATCH 03/15] add date for movement editor --- .../vuejs/HouseholdMembersEditor/App.vue | 3 ++ .../components/Dates.vue | 36 +++++++++++++++++++ .../HouseholdMembersEditor/store/index.js | 13 +++++-- 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue index e5d84e920..939dc6119 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -1,6 +1,7 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js index f5db83c66..74cab3b1d 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -6,7 +6,6 @@ const concerned = window.household_members_editor_data.persons.map(p => { return { person: p, position: null, - start_date: null, allowRemove: false, }; }); @@ -17,10 +16,12 @@ const store = createStore({ concerned, household: window.household_members_editor_data.household, positions: window.household_members_editor_data.positions, + startDate: new Date(), allowHouseholdCreate: window.household_members_editor_data.allowHouseholdCreate, allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, - forceLeaveWithoutHousehold: false + forceLeaveWithoutHousehold: false, + }, getters: { isHouseholdNew(state) { @@ -68,7 +69,7 @@ const store = createStore({ let persons = state.concerned.map(conc => conc.person.id); if (!persons.includes(person.id)) { state.concerned.push({ person, position: null, - start_date: null, allowRemove: true }); + allowRemove: true }); } else { console.err("person already included"); } @@ -105,6 +106,9 @@ const store = createStore({ state.household = null; state.forceLeaveWithoutHousehold = true; }, + setStartDate(state, dateI) { + state.startDate = dateI + }, }, actions: { addConcerned({ commit }, person) { @@ -132,6 +136,9 @@ const store = createStore({ forceLeaveWithoutHousehold({ commit }) { commit('forceLeaveWithoutHousehold'); }, + setStartDate({ commit }, date) { + commit('setStartDate', date); + }, } }); From 48e58090087d527336936f8a15a62f88b8ec15a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 4 Jun 2021 21:23:51 +0200 Subject: [PATCH 04/15] add utility for date --- .../Resources/public/js/date.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/Bundle/ChillMainBundle/Resources/public/js/date.js diff --git a/src/Bundle/ChillMainBundle/Resources/public/js/date.js b/src/Bundle/ChillMainBundle/Resources/public/js/date.js new file mode 100644 index 000000000..35499e1aa --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/js/date.js @@ -0,0 +1,64 @@ +/** + * Some utils for manipulating dates + */ + +/** + * Return the date to local ISO date, like YYYY-mm-dd + * + * The date is valid for the same timezone as the date's locale + * + * Do not take time into account + */ +const dateToISO = (date) => { + return [ + this.$store.state.startDate.getFullYear(), + (this.$store.state.startDate.getMonth() + 1).toString().padStart(2, '0'), + this.$store.state.startDate.getDate().toString().padStart(2, '0') + ].join('-'); +}; + +/** + * Return a date object from iso string formatted as YYYY-mm-dd + */ +const ISOToDate = (str) => { + let + [year, month, day] = str.split('-'); + + return new Date(year, month-1, day); +} + +/** + * Convert a date to ISO8601, valid for usage in api + */ +const datetimeToISO = (date) => { + let cal, time, offset; + cal = [ + date.getFullYear(), + (date.getMonth() + 1).toString().padStart(2, '0'), + date.getDate().toString().padStart(2, '0') + ].join('-'); + + time = [ + date.getHours().toString().padStart(2, '0'), + date.getMinutes().toString().padStart(2, '0'), + date.getSeconds().toString().padStart(2, '0') + ].join(':'); + + offset = [ + date.getTimezoneOffset() <= 0 ? '+' : '-', + Math.abs(Math.floor(date.getTimezoneOffset() / 60)).toString().padStart(2, '0'), + ':', + Math.abs(date.getTimezoneOffset() % 60).toString().padStart(2, '0'), + ].join(''); + + let x = cal + 'T' + time + offset; + console.log('return date', x); + + return x; +}; + +export { + dateToISO, + ISOToDate, + datetimeToISO +}; From e5905902cc5b02c337a6694c93149c8a57e3f2aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Fri, 4 Jun 2021 21:24:11 +0200 Subject: [PATCH 05/15] add confirm button for move --- .../Controller/HouseholdMemberController.php | 43 +++++++ .../vuejs/HouseholdMembersEditor/App.vue | 3 + .../vuejs/HouseholdMembersEditor/api.js | 47 ++++++++ .../components/Confirmation.vue | 32 +++++ .../HouseholdMembersEditor/store/index.js | 111 ++++++++++++++++-- .../ChillPersonBundle/chill.api.specs.yaml | 42 +++++++ 6 files changed, 268 insertions(+), 10 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/api.js create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Confirmation.vue diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php index 24d77467d..e60deea8e 100644 --- a/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdMemberController.php @@ -50,6 +50,49 @@ class HouseholdMemberController extends ApiController return $this->json($editor->getHousehold(), Response::HTTP_OK, ["groups" => ["read"]]); } + /** + * @Route( + * "/api/1.0/person/household/members/move/test.{_format}", + * name="chill_api_person_household_members_move_test" + * ) + */ + public function test(Request $request, $_format): Response + { + try { + $editor = $this->getSerializer() + ->deserialize($request->getContent(), MembersEditor::class, + $_format, ['groups' => [ "read" ]]); + } catch (Exception\InvalidArgumentException | Exception\UnexpectedValueException $e) { + throw new BadRequestException("Deserialization error: {$e->getMessage()}", 45896, $e); + } + // TODO ACL + // + // TODO validation + // temporary, to have at least one problem for testing purpose + /*$violations = [ + new \Symfony\Component\Validator\ConstraintViolation( + "This is a fake message", + null, + [], + $editor->getHousehold(), + 'household.members.startDate', + new \DateTime('10 years ago') + ), + new \Symfony\Component\Validator\ConstraintViolation( + "This is another fake message", + null, + [], + $editor->getHousehold(), + 'household.members.endDate', + new \DateTime('10 years ago') + ) + ]; + $violationsList = new \Symfony\Component\Validator\ConstraintViolationList($violations); + */ + + return $this->json($editor->getHousehold(), Response::HTTP_OK, ["groups" => ["read"]]); + } + /** * @Route( * "/{_locale}/person/household/members/editor", diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue index 939dc6119..7fc138ee9 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -2,6 +2,7 @@ + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js index 74cab3b1d..447bac285 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -1,4 +1,6 @@ import { createStore } from 'vuex'; +import { householdMove, householdMoveTest } from './../api.js'; +import { datetimeToISO } from 'ChillMainAssets/js/date.js'; const debug = process.env.NODE_ENV !== 'production'; @@ -7,6 +9,7 @@ const concerned = window.household_members_editor_data.persons.map(p => { person: p, position: null, allowRemove: false, + holder: false }; }); @@ -21,7 +24,7 @@ const store = createStore({ allowHouseholdSearch: window.household_members_editor_data.allowHouseholdSearch, allowLeaveWithoutHousehold: window.household_members_editor_data.allowLeaveWithoutHousehold, forceLeaveWithoutHousehold: false, - + warnings: [], }, getters: { isHouseholdNew(state) { @@ -63,13 +66,51 @@ const store = createStore({ .find(conc => conc.person.id === person_id) ; }, + buildPayload: (state) => { + let + conc, + payload = { + concerned: [], + destination: { + id: state.household.id, + type: state.household.type + } + } + ; + + for (let i in state.concerned) { + conc = state.concerned[i]; + console.log('loop', conc); + payload.concerned.push({ + person: { + id: conc.person.id, + type: conc.person.type + }, + position: { + id: conc.position.id, + type: conc.position.type + }, + holder: conc.holder, + comment: "", + start_date: { + datetime: datetimeToISO(state.startDate) + } + }); + } + + return payload; + }, }, mutations: { addConcerned(state, person) { let persons = state.concerned.map(conc => conc.person.id); if (!persons.includes(person.id)) { - state.concerned.push({ person, position: null, - allowRemove: true }); + state.concerned.push({ + person, + position: null, + allowRemove: true, + holder: false + }); } else { console.err("person already included"); } @@ -107,39 +148,89 @@ const store = createStore({ state.forceLeaveWithoutHousehold = true; }, setStartDate(state, dateI) { - state.startDate = dateI + state.startDate = dateI; + }, + setWarnings(state, warnings) { + state.warnings = warnings; }, }, actions: { - addConcerned({ commit }, person) { + addConcerned({ commit, dispatch }, person) { console.log('from actions addConcerned'); commit('addConcerned', person); + dispatch('computeWarnings'); }, - markPosition({ commit, state }, { person_id, position_id }) { + markPosition({ commit, state, dispatch }, { person_id, position_id }) { console.log('from action markPosition'); console.log('person_id', person_id); console.log('position_id', position_id); commit('markPosition', { person_id, position_id }); + dispatch('computeWarnings'); }, toggleHolder({ commit }, conc) { commit('toggleHolder', conc); }, - removePosition({ commit }, conc) { + removePosition({ commit, dispatch }, conc) { commit('removePosition', conc); + dispatch('computeWarnings'); }, - removeConcerned({ commit }, conc) { + removeConcerned({ commit, dispatch }, conc) { commit('removeConcerned', conc); + dispatch('computeWarnings'); }, - createHousehold({ commit }) { + createHousehold({ commit, dispatch }) { commit('createHousehold'); + dispatch('computeWarnings'); }, - forceLeaveWithoutHousehold({ commit }) { + forceLeaveWithoutHousehold({ commit, dispatch }) { commit('forceLeaveWithoutHousehold'); + dispatch('computeWarnings'); }, setStartDate({ commit }, date) { commit('setStartDate', date); }, + computeWarnings({ commit, state, getters }) { + let warnings = [], + payload; + + if (!getters.hasHousehold && !state.forceLeaveWithoutHousehold) { + warnings.push({ m: 'household_member_editor.add_destination', a: {} }); + } + + if (state.concerned.length === 0) { + warnings.push({ m: 'household_member_editor.add_at_least_onePerson', a: {} }); + } + + if (getters.concUnpositionned.length > 0 + && !state.forceLeaveWithoutHousehold) { + warnings.push({ m: 'household_member_editor.give_a_position_to_every_person', a: {} }) + } + + if (warnings.length === 0) { + payload = getters.buildPayload; + householdMoveTest(payload).then(errors => { + for (let i in errors.violations) { + console.log('error from server', errors.violations[i]); + warnings.push({ m: errors.violations[i].title, a: {} }); + } + commit('setWarnings', warnings); + }); + } else { + commit('setWarnings', warnings); + } + }, + confirm({ getters }) { + let payload = getters.buildPayload; + householdMove(payload).then(household => { + console.log('move success', household); + let id = household.id; + // nothing to do anymore here, bye-bye ! + window.location.replace(`/fr/person/household/{id}/members`); + }); + }, } }); +store.dispatch('computeWarnings'); + export { store }; diff --git a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml index 0fac8e6ce..30178e385 100644 --- a/src/Bundle/ChillPersonBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillPersonBundle/chill.api.specs.yaml @@ -826,3 +826,45 @@ paths: 400: description: "transition cannot be applyed" + /1.0/person/household/members/move/test.json: + post: + tags: + - household + summary: test the move of one or more person, without persisting changes in database + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + concerned: + type: array + items: + type: object + properties: + person: + $ref: '#/components/schemas/PersonById' + start_date: + $ref: '#/components/schemas/Date' + position: + $ref: '#/components/schemas/HouseholdPosition' + holder: + type: boolean + comment: + type: string + destination: + oneOf: + - $ref: '#/components/schemas/Household' + responses: + 401: + description: "Unauthorized" + 404: + description: "Not found" + 200: + description: "OK" + 422: + description: "Unprocessable entity (validation errors)" + 400: + description: "transition cannot be applyed" + From e14205ae1d345241d7a9e496d26b33924b149f65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 9 Jun 2021 16:13:39 +0200 Subject: [PATCH 06/15] first layout for form edit --- .../Resources/public/img/draggable.svg | 121 ++++++++++++++++++ .../Resources/public/js/date.js | 28 +++- .../public/modules/bootstrap/bootstrap.scss | 4 +- .../Resources/public/scss/chillmain.scss | 2 +- .../Resources/public/vuejs/_js/i18n.js | 2 +- .../public/sass/person_with_period.scss | 4 +- .../components/Concerned.vue | 110 +++++++++++----- .../components/Confirmation.vue | 22 +++- .../components/Dates.vue | 4 +- .../components/Household.vue | 12 +- .../components/MemberDetails.vue | 83 +++++++++--- .../vuejs/HouseholdMembersEditor/js/i18n.js | 22 +++- .../HouseholdMembersEditor/store/index.js | 19 +-- .../vuejs/_components/Person/Person.vue | 16 +++ .../Resources/public/vuejs/_js/i18n.js | 10 +- .../views/Household/members_editor.html.twig | 10 +- 16 files changed, 386 insertions(+), 83 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/_components/Person/Person.vue diff --git a/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg b/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg new file mode 100644 index 000000000..0b3561cce --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/img/draggable.svg @@ -0,0 +1,121 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/js/date.js b/src/Bundle/ChillMainBundle/Resources/public/js/date.js index 35499e1aa..7b9bf88a2 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/js/date.js +++ b/src/Bundle/ChillMainBundle/Resources/public/js/date.js @@ -1,5 +1,7 @@ /** * Some utils for manipulating dates + * + * **WARNING** experimental */ /** @@ -8,6 +10,8 @@ * The date is valid for the same timezone as the date's locale * * Do not take time into account + * + * **Experimental** */ const dateToISO = (date) => { return [ @@ -19,16 +23,36 @@ const dateToISO = (date) => { /** * Return a date object from iso string formatted as YYYY-mm-dd + * + * **Experimental** */ const ISOToDate = (str) => { - let + let [year, month, day] = str.split('-'); return new Date(year, month-1, day); } +/** + * Return a date object from iso string formatted as YYYY-mm-dd:HH:MM:ss+01:00 + * + * **Experimental** + */ +const ISOToDatetime = (str) => { + console.log(str); + let + [cal, times] = str.split('T'), + [year, month, date] = cal.split('-'), + [time, timezone] = cal.split(times.charAt(9)), + [hours, minutes, seconds] = cal.split(':') + ; + + return new Date(year, month-1, date, hours, minutes, seconds); +} + /** * Convert a date to ISO8601, valid for usage in api + * */ const datetimeToISO = (date) => { let cal, time, offset; @@ -52,7 +76,6 @@ const datetimeToISO = (date) => { ].join(''); let x = cal + 'T' + time + offset; - console.log('return date', x); return x; }; @@ -60,5 +83,6 @@ const datetimeToISO = (date) => { export { dateToISO, ISOToDate, + ISOToDatetime, datetimeToISO }; diff --git a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss index 37bf2f253..6b91d1bf0 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/modules/bootstrap/bootstrap.scss @@ -17,7 +17,7 @@ // @import "bootstrap/scss/grid"; // @import "bootstrap/scss/tables"; // @import "bootstrap/scss/forms"; -// @import "bootstrap/scss/buttons"; +@import "bootstrap/scss/buttons"; @import "bootstrap/scss/transitions"; // @import "bootstrap/scss/dropdown"; // @import "bootstrap/scss/button-group"; @@ -30,7 +30,7 @@ // @import "bootstrap/scss/pagination"; @import "bootstrap/scss/badge"; // @import "bootstrap/scss/jumbotron"; -// @import "bootstrap/scss/alert"; +@import "bootstrap/scss/alert"; // @import "bootstrap/scss/progress"; // @import "bootstrap/scss/media"; // @import "bootstrap/scss/list-group"; diff --git a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss index 2eb1f9bc7..1f4124f44 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/scss/chillmain.scss @@ -112,7 +112,7 @@ div.flex-bloc { display: flex; flex-direction: column; - div.item-row { + & > div.item-row { flex-grow: 1; flex-shrink: 1; flex-basis: auto; display: flex; flex-direction: column; diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js index 025aeeb44..d89470bc4 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_js/i18n.js @@ -62,7 +62,7 @@ const messages = { person: "un nouvel usager", thirdparty: "un nouveau tiers" }, - } + }, } }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss b/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss index dc1c96274..bea5bdba2 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss +++ b/src/Bundle/ChillPersonBundle/Resources/public/sass/person_with_period.scss @@ -47,7 +47,9 @@ div.list-household-members--summary { .chill-entity__person { .chill-entity__person__first-name, - .chill-entity__person__last-name { + .chill-entity__person__last-name, + // text is used in vue component 'Person' + .chill-entity__person__text { font-size: 1.3em; font-weight: 700; } diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue index 8757febcd..3008441ee 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue @@ -1,25 +1,56 @@ diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js index 0f019c26b..f83004ce8 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/_js/i18n.js @@ -15,7 +15,15 @@ const personMessages = { person: { firstname: "Prénom", lastname: "Nom", - born: "né{e} le ", + born: (ctx) => { + if (ctx.gender === 'man') { + return 'Né le'; + } else if (ctx.gender === 'woman') { + return 'Née le'; + } else { + return 'Né·e le'; + } + }, center_id: "Identifiant du centre", center_type: "Type de centre", center_name: "Territoire", // vendée diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig index f0433a812..d409ec38f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig @@ -1,8 +1,14 @@ {% extends '@ChillMain/layout.html.twig' %} +{% block title 'household.Edit household members'|trans %} + {% block content %} -

Editor

-
+
+
+

{{ block('title') }}

+
+
+
{% endblock %} {% block js %} From 71f794c4d94aac2213598d4e812ec8a12f4bb6b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 9 Jun 2021 17:31:55 +0200 Subject: [PATCH 07/15] add ckeditor for comment --- .../components/Concerned.vue | 7 ----- .../components/MemberDetails.vue | 26 +++++++++++++++-- .../HouseholdMembersEditor/store/index.js | 29 +++++++------------ 3 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue index 3008441ee..cb422535f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue @@ -76,8 +76,6 @@ v-for="conc in concByPosition(position.id)" v-bind:key="conc.person.id" v-bind:conc="conc" - draggable="true" - @dragstart="onStartDragConcern($event, conc.person.id)" >
- {{ $t('household_members_editor.holder') }} @@ -21,6 +20,10 @@
+
+ +
+