From 5423de3bd95958160067fd5a0fb09c37d1f2705f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 14:23:54 +0100 Subject: [PATCH 01/21] fields added to person creation form --- .../ChillPersonBundle/Form/CreationPersonType.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 3d409129b..82437eb30 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -22,6 +22,8 @@ use Chill\PersonBundle\Security\Authorization\PersonVoter; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\TelType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -52,11 +54,20 @@ final class CreationPersonType extends AbstractType $builder ->add('firstName') ->add('lastName') + ->add('gender', GenderType::class, [ + 'required' => true, 'placeholder' => null, + ]) ->add('birthdate', ChillDateType::class, [ 'required' => false, ]) - ->add('gender', GenderType::class, [ - 'required' => true, 'placeholder' => null, + ->add('phonenumber', TelType::class, [ + 'required' => false + ]) + ->add('mobilenumber', TelType::class, [ + 'required' => false + ]) + ->add('email', EmailType::class, [ + 'required' => false ]); if ($this->askCenters) { From 9a3f35703b0dd08fd103d78ec341461232970d7a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 14:24:46 +0100 Subject: [PATCH 02/21] parameter passed to person creation page --- src/Bundle/ChillPersonBundle/Controller/PersonController.php | 1 + .../Resources/views/Person/list_with_period.html.twig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index 02c77ebd3..6e444651b 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -266,6 +266,7 @@ final class PersonController extends AbstractController [ 'form' => $form->createView(), 'alternatePersons' => $alternatePersons ?? [], + '_fragment' => $request->query->get('_fragment') ] ); } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig index 4b0076078..46ef45fc0 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig @@ -26,7 +26,7 @@ + From 96b1f31665d1bb1aa395af41a19241563cfe9d71 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 16:24:35 +0100 Subject: [PATCH 04/21] javascript added to easily fill in form fields with name elements --- .../Controller/PersonController.php | 1 - .../Resources/views/Person/create.html.twig | 54 ++++++++++++++++++- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Controller/PersonController.php b/src/Bundle/ChillPersonBundle/Controller/PersonController.php index 6e444651b..02c77ebd3 100644 --- a/src/Bundle/ChillPersonBundle/Controller/PersonController.php +++ b/src/Bundle/ChillPersonBundle/Controller/PersonController.php @@ -266,7 +266,6 @@ final class PersonController extends AbstractController [ 'form' => $form->createView(), 'alternatePersons' => $alternatePersons ?? [], - '_fragment' => $request->query->get('_fragment') ] ); } diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index 921ae497d..ddaabe102 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -63,9 +63,21 @@ {{ form_start(form, {'attr' : {'id' : 'create-form'}}) }} - {{ form_row(form.lastName, { 'label' : 'Last name'|trans }) }} +
+ {{ form_label(form.lastName, 'Last name'|trans) }} +
+ {{ form_widget(form.lastName, { 'id' : 'lastname-field'}) }} +
+
+
- {{ form_row(form.firstName, { 'label' : 'First name'|trans }) }} +
+ {{ form_label(form.firstName, 'First name'|trans) }} +
+ {{ form_widget(form.firstName, { 'id' : 'firstname-field' }) }} +
+
+
{% if form.altNames is defined %} {{ form_widget(form.altNames) }} @@ -109,4 +121,42 @@ {% block js %} {# {{ encore_entry_script_tags('mod_disablebuttons') }} #} + + {% endblock js %} From cf8e25e8235f38a02e9271e8777c3d29dcce6331 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 16:26:51 +0100 Subject: [PATCH 05/21] search parameter also passed on in normal list template --- .../ChillPersonBundle/Resources/views/Person/list.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list.html.twig index 564bff97d..5423388e6 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list.html.twig @@ -83,7 +83,7 @@
    {% if is_granted('CHILL_PERSON_CREATE') %}
  • - + {{ 'Add a person'|trans }}
  • From 169442decc6afa5c4eba8a4d8d22a3d274eddf40 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 16:29:46 +0100 Subject: [PATCH 06/21] changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 568224874..cc4dbdb96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to * [main] Add mainLocation field to User entity and add it in user form type * [course list in person context] show full username/label for ref * [accompanying period work] remove the possibility to generate document from an accompanying period work +* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) ## Test releases From eaf9f72fdda66239bf2703b4aaa0b911e0fc9e70 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 16:34:54 +0100 Subject: [PATCH 07/21] lastname added in uppercase --- .../Resources/views/Person/create.html.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index ddaabe102..785b07bb0 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -143,18 +143,18 @@ const tag = tags[i] tag.onclick = function(e) { const field = e.target.parentElement.id === 'suggest-lastname' ? document.getElementById('lastname-field') : document.getElementById('firstname-field') + const suggestion = e.target.parentElement.id === 'suggest-lastname' ? e.target.textContent.toUpperCase() : e.target.textContent if (field.value === '') { - field.value = e.target.textContent; + field.value = suggestion; } else { - field.value = `${field.value} ${e.target.textContent}` + field.value = `${field.value} ${suggestion}` } e.target.style.display = "none"; [...document.querySelectorAll("p")] .filter(p => p.textContent.includes(e.target.textContent)) .forEach(p => p.style.display = "none") - } } From 21d5f974ebdba1759ba47a6b148267160f71476a Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Tue, 18 Jan 2022 10:20:45 +0100 Subject: [PATCH 08/21] advanced search possible --- .../Resources/views/Person/create.html.twig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index 785b07bb0..dd638fafd 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -126,7 +126,15 @@ return string.charAt(0).toUpperCase() + string.slice(1); } - const searchFragments = location.hash.substring(1).split('%20') + let searchFragments = location.hash.substring(1).split('%20') + searchFragments = searchFragments.filter((el) => el.startsWith("firstname") || el.startsWith("lastname")) + searchFragments = searchFragments.map((el) => { + if (el.startsWith("firstname")) { + return el.slice(10) + } else { + return el.slice(9) + } + }) if (searchFragments) { const suggestions = searchFragments.map((el) => `

    ${capitalizeFirstLetter(el)}

    `) From 2811e6143927fa66e3bc7acdbf5d092540a90163 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jan 2022 15:13:22 +0100 Subject: [PATCH 09/21] fix filtering of names --- .../Resources/views/Person/create.html.twig | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index dd638fafd..3b5d3abbc 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -126,16 +126,29 @@ return string.charAt(0).toUpperCase() + string.slice(1); } - let searchFragments = location.hash.substring(1).split('%20') - searchFragments = searchFragments.filter((el) => el.startsWith("firstname") || el.startsWith("lastname")) + const uri = decodeURI(location.hash.substring(1)) + console.log(uri) + let searchFragments = uri.split(' ') + console.log(searchFragments) + searchFragments = searchFragments.filter((el) => { + if ( ( el.startsWith("firstname") || el.startsWith("lastname") ) || (el !== '' && !el.startsWith('birthdate') && !el.startsWith('gender') && !el.startsWith('city') && !el.startsWith('phonenumber') && !el.startsWith('@'))) { + return el + } + }) + + console.log('after filter', searchFragments) + searchFragments = searchFragments.map((el) => { if (el.startsWith("firstname")) { return el.slice(10) - } else { - return el.slice(9) + } else if (el.startsWith("lastname")) { + return el.slice(10) } + return el.replace('\"', '') }) + console.log(searchFragments) + if (searchFragments) { const suggestions = searchFragments.map((el) => `

    ${capitalizeFirstLetter(el)}

    `) const suggestFirstName = document.getElementById("suggest-firstname") @@ -145,12 +158,11 @@ suggestLastname.innerHTML = suggestions.join(' '); } - const tags = document.getElementsByClassName('name') + const tags = document.querySelectorAll('.name') - for (let i=0; i < tags.length; i++) { - const tag = tags[i] - tag.onclick = function(e) { - const field = e.target.parentElement.id === 'suggest-lastname' ? document.getElementById('lastname-field') : document.getElementById('firstname-field') + tags.forEach((tag) => { + tag.addEventListener('click', function(e) { + const field = e.target.parentElement.id === 'suggest-lastname' ? document.getElementById('lastname-field') : document.getElementById('firstname-field') const suggestion = e.target.parentElement.id === 'suggest-lastname' ? e.target.textContent.toUpperCase() : e.target.textContent if (field.value === '') { @@ -163,8 +175,9 @@ [...document.querySelectorAll("p")] .filter(p => p.textContent.includes(e.target.textContent)) .forEach(p => p.style.display = "none") - } - } + }) + }) + {% endblock js %} From 409cd40460b38929f025ce332585d36127480f46 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Wed, 19 Jan 2022 15:37:30 +0100 Subject: [PATCH 10/21] js code put in seperate file for compilation --- .../public/page/person/suggest-names.js | 60 ++++++++++++++++++ .../Resources/views/Person/create.html.twig | 61 +------------------ .../ChillPersonBundle/chill.webpack.config.js | 1 + 3 files changed, 62 insertions(+), 60 deletions(-) create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js diff --git a/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js b/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js new file mode 100644 index 000000000..fedef3c13 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js @@ -0,0 +1,60 @@ +function capitalizeFirstLetter(string) { + return string.charAt(0).toUpperCase() + string.slice(1); +} + +window.addEventListener('DOMContentLoaded', function() { + + const uri = decodeURI(location.hash.substring(1)) + // console.log(uri) + let searchFragments = uri.split(' ') + console.log(searchFragments) + searchFragments = searchFragments.filter((el) => { + if ( ( el.startsWith("firstname") || el.startsWith("lastname") ) || (el !== '' && !el.startsWith('birthdate') && !el.startsWith('gender') && !el.startsWith('city') && !el.startsWith('phonenumber') && !el.startsWith('@'))) { + return el + } + }) + + console.log('after filter', searchFragments) + + searchFragments = searchFragments.map((el) => { + if (el.startsWith("firstname")) { + return el.slice(10) + } else if (el.startsWith("lastname")) { + return el.slice(10) + } + return el.replace('\"', '') + }) + + console.log(searchFragments) + + if (searchFragments) { + const suggestions = searchFragments.map((el) => `

    ${capitalizeFirstLetter(el)}

    `) + const suggestFirstName = document.getElementById("suggest-firstname") + const suggestLastname = document.getElementById("suggest-lastname") + + suggestFirstName.innerHTML = suggestions.join(' '); + suggestLastname.innerHTML = suggestions.join(' '); + } + + const tags = document.querySelectorAll('.name') + + tags.forEach((tag) => { + tag.addEventListener('click', function(e) { + const field = e.target.parentElement.id === 'suggest-lastname' ? document.getElementById('lastname-field') : document.getElementById('firstname-field') + const suggestion = e.target.parentElement.id === 'suggest-lastname' ? e.target.textContent.toUpperCase() : e.target.textContent + + if (field.value === '') { + field.value = suggestion; + } else { + field.value = `${field.value} ${suggestion}` + } + e.target.style.display = "none"; + + [...document.querySelectorAll("p")] + .filter(p => p.textContent.includes(e.target.textContent)) + .forEach(p => p.style.display = "none") + }) + }) + +}) + diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index 3b5d3abbc..880fac4fb 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -120,64 +120,5 @@ {% endblock content %} {% block js %} - {# {{ encore_entry_script_tags('mod_disablebuttons') }} #} - - + {{ encore_entry_script_tags('page_suggest_names') }} {% endblock js %} diff --git a/src/Bundle/ChillPersonBundle/chill.webpack.config.js b/src/Bundle/ChillPersonBundle/chill.webpack.config.js index 7a8b79c7c..0b83f8e41 100644 --- a/src/Bundle/ChillPersonBundle/chill.webpack.config.js +++ b/src/Bundle/ChillPersonBundle/chill.webpack.config.js @@ -18,4 +18,5 @@ module.exports = function(encore, entries) 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_accompanying_course_index_masonry', __dirname + '/Resources/public/page/accompanying_course_index/masonry.js'); + encore.addEntry('page_suggest_names', __dirname + '/Resources/public/page/person/suggest-names.js'); }; From 03bd4d1942ce2273f1356374f64e5f01be466623 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jan 2022 17:46:27 +0100 Subject: [PATCH 11/21] update logic to adapt to altnames --- .../public/page/person/suggest-names.js | 41 ++++++++++--------- .../Resources/views/Person/create.html.twig | 18 +++++--- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js b/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js index fedef3c13..8973d0e6c 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/page/person/suggest-names.js @@ -1,21 +1,17 @@ function capitalizeFirstLetter(string) { - return string.charAt(0).toUpperCase() + string.slice(1); + return string.charAt(0).toLocaleUpperCase() + string.slice(1); } window.addEventListener('DOMContentLoaded', function() { const uri = decodeURI(location.hash.substring(1)) - // console.log(uri) let searchFragments = uri.split(' ') - console.log(searchFragments) searchFragments = searchFragments.filter((el) => { if ( ( el.startsWith("firstname") || el.startsWith("lastname") ) || (el !== '' && !el.startsWith('birthdate') && !el.startsWith('gender') && !el.startsWith('city') && !el.startsWith('phonenumber') && !el.startsWith('@'))) { return el } }) - console.log('after filter', searchFragments) - searchFragments = searchFragments.map((el) => { if (el.startsWith("firstname")) { return el.slice(10) @@ -25,34 +21,39 @@ window.addEventListener('DOMContentLoaded', function() { return el.replace('\"', '') }) - console.log(searchFragments) - if (searchFragments) { - const suggestions = searchFragments.map((el) => `

    ${capitalizeFirstLetter(el)}

    `) - const suggestFirstName = document.getElementById("suggest-firstname") - const suggestLastname = document.getElementById("suggest-lastname") + const pre = '
      '; + const after = '
    '; - suggestFirstName.innerHTML = suggestions.join(' '); - suggestLastname.innerHTML = suggestions.join(' '); + document.querySelectorAll('[data-suggest-container]').forEach(function(container) { + const suggestions = searchFragments.map((el) => `
  • ${capitalizeFirstLetter(el)}
  • `); + container.innerHTML = pre + suggestions.join(' ') + after; + }) } - const tags = document.querySelectorAll('.name') - - tags.forEach((tag) => { + const tags = document.querySelectorAll('[data-suggest-target]').forEach((tag) => { tag.addEventListener('click', function(e) { - const field = e.target.parentElement.id === 'suggest-lastname' ? document.getElementById('lastname-field') : document.getElementById('firstname-field') - const suggestion = e.target.parentElement.id === 'suggest-lastname' ? e.target.textContent.toUpperCase() : e.target.textContent + const field = document.querySelector(`[name="${e.target.dataset.suggestTarget}"]`); + let suggestion = e.target.textContent.trim(); + switch (field.dataset.suggestTransform) { + case 'uppercase_all': + suggestion = suggestion.toLocaleUpperCase(); + break; + case 'uppercase_first_letter': + default: + suggestion = capitalizeFirstLetter(suggestion); + } - if (field.value === '') { + if (field.value === '') { field.value = suggestion; } else { field.value = `${field.value} ${suggestion}` } e.target.style.display = "none"; - [...document.querySelectorAll("p")] + [...document.querySelectorAll("[data-suggest-target]")] .filter(p => p.textContent.includes(e.target.textContent)) - .forEach(p => p.style.display = "none") + .forEach(p => p.remove()); }) }) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig index 880fac4fb..70d6a4a8e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/create.html.twig @@ -66,21 +66,29 @@
    {{ form_label(form.lastName, 'Last name'|trans) }}
    - {{ form_widget(form.lastName, { 'id' : 'lastname-field'}) }} + {{ form_widget(form.lastName, {'attr': {'data-suggest-transform': 'uppercase_all' } }) }}
    -
    +
    {{ form_label(form.firstName, 'First name'|trans) }}
    - {{ form_widget(form.firstName, { 'id' : 'firstname-field' }) }} + {{ form_widget(form.firstName, {'attr': {'data-suggest-transform': 'uppercase_first_letter' } }) }}
    -
    +
    {% if form.altNames is defined %} - {{ form_widget(form.altNames) }} + {% for altName in form.altNames %} +
    + {{ form_label(altName) }} +
    + {{ form_widget(altName, {'attr': {'data-suggest-transform': 'uppercase_all' } }) }} +
    +
    +
    + {% endfor %} {% endif %} {{ form_row(form.gender, { 'label' : 'Gender'|trans }) }} From c736c2b5bbbe369b8d4f77b55d73436fcf5ec4a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Wed, 19 Jan 2022 17:48:46 +0100 Subject: [PATCH 12/21] update changelog --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 093a3dd96..8ad04a636 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,16 +11,18 @@ and this project adheres to ## Unreleased +* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) + +## Test releases + +### test release 2022-01-19 * vuejs: add dead information on all on-the-fly person render boxes, in vis graph and other templates (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/271) * [thirdparty] fix bug in 3rd party view: types was replaced by thirdPartyTypes * [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246) * [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176) * [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380) -* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) -## Test releases - -### test release 2022-01-10 +### test release 2022-01-17 * [main] Add editableByUser field to locationType entity, adapt the admin template and add this condition in the location-type endpoint (see https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/297) * [main] Add mainLocation field to User entity and add it in user form type @@ -32,7 +34,6 @@ and this project adheres to ## Test releases * vuejs: add validation on required fields for AddPerson, Address and Location components * vuejs: treat 422 validation errors in locations and AddPerson components ->>>>>>> origin/master ### test release 2022-01-12 From fcf2ae364f54969ed7cc04ff35079da033e1a38d Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 21 Jan 2022 16:47:02 +0100 Subject: [PATCH 13/21] all comments are kept when after a second is flagged --- src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 06e4f0c8b..ac39f2461 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -1099,6 +1099,10 @@ class AccompanyingPeriod implements } if ($comment instanceof Comment) { + + if (null !== $this->pinnedComment) { + $this->addComment($this->pinnedComment); + } $this->addComment($comment); } From 2b9f0e51777ac85831f7054bde495810a8c5de46 Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 21 Jan 2022 16:50:09 +0100 Subject: [PATCH 14/21] changelog updated --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad04a636..54447b427 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ and this project adheres to * [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246) * [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176) * [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380) +* [parcours]: bug fix when comment is pinned all other comments remain in the collection (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/385) ### test release 2022-01-17 From 45f3bb00d6ffb4b462446fedb7ba0dce8a31238e Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 21 Jan 2022 16:50:24 +0100 Subject: [PATCH 15/21] php csfixes --- src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php | 1 - src/Bundle/ChillPersonBundle/Form/CreationPersonType.php | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index ac39f2461..752328511 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -1099,7 +1099,6 @@ class AccompanyingPeriod implements } if ($comment instanceof Comment) { - if (null !== $this->pinnedComment) { $this->addComment($this->pinnedComment); } diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 82437eb30..a33858c02 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -61,13 +61,13 @@ final class CreationPersonType extends AbstractType 'required' => false, ]) ->add('phonenumber', TelType::class, [ - 'required' => false + 'required' => false, ]) ->add('mobilenumber', TelType::class, [ - 'required' => false + 'required' => false, ]) ->add('email', EmailType::class, [ - 'required' => false + 'required' => false, ]); if ($this->askCenters) { From cdbae1eade64cd52eb148894a7dc9c4715767e7b Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Fri, 21 Jan 2022 17:00:04 +0100 Subject: [PATCH 16/21] bugfix in parcours display if deathdate of associated person is not defined (ex. for thirdparty) --- CHANGELOG.md | 1 + .../Resources/views/AccompanyingCourse/index.html.twig | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ad04a636..549276554 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to * [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) +* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before. ## Test releases diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig index c0de1e1ba..1fa8c52e8 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/index.html.twig @@ -9,7 +9,7 @@ action: 'show', displayBadge: true, targetEntity: { name: type, id: entity.id }, buttonText: entity|chill_entity_render_string, - isDead: entity.deathdate is not null, + isDead: entity.deathdate is defined and entity.deathdate is not null, parent: parent } %} {% endmacro %} From ad05b3bf05ed0e47986ce4c9c5354e0c3921a5e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Sun, 23 Jan 2022 22:24:48 +0100 Subject: [PATCH 17/21] fix variable name in work list page --- .../Resources/views/AccompanyingCourseWork/index.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig index 3bb34a22d..83a047c98 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig @@ -70,7 +70,7 @@ action: 'show', displayBadge: true, targetEntity: { name: 'person', id: p.id }, buttonText: p|chill_entity_render_string, - isDead: entity.deathdate is not null + isDead: p.deathdate is not null } %} {% endfor %} From 54c2b92962bdc33ed23e05148ffc04d2b5e0fda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 24 Jan 2022 10:09:57 +0000 Subject: [PATCH 18/21] Improve notifications --- CHANGELOG.md | 6 ++ .../Controller/ActivityController.php | 23 ++--- .../Resources/views/Activity/list.html.twig | 98 ++++++++++--------- .../ChillMainExtension.php | 1 + .../ChillMainBundle/Entity/Notification.php | 3 + .../Notification/Email/NotificationMailer.php | 10 +- .../Notification/NotificationPresence.php | 26 +++++ .../Templating/NotificationTwigExtension.php | 7 ++ .../NotificationTwigExtensionRuntime.php | 15 +++ .../Repository/NotificationRepository.php | 59 ++++++++--- .../views/Notification/_list_item.html.twig | 9 +- .../views/Notification/create.html.twig | 4 +- ...ension_counter_notifications_for.html.twig | 2 + ...extension_list_notifications_for.html.twig | 2 +- .../Service/Mailer/ChillMailer.php | 50 ++++++++++ .../config/services/mailer.yaml | 10 ++ .../migrations/Version20220120155303.php | 33 +++++++ .../translations/messages+intl-icu.fr.yaml | 16 +++ .../translations/messages.fr.yml | 1 + .../UserRefEventSubscriber.php} | 54 +++++++--- .../Entity/AccompanyingPeriod.php | 21 ++++ .../Form/CreationPersonType.php | 6 +- .../AccompanyingCourseWork/index.html.twig | 4 + .../views/AccompanyingPeriod/_list.html.twig | 4 + .../views/Person/list_with_period.html.twig | 4 + .../config/services/accompanyingPeriod.yaml | 14 ++- 26 files changed, 385 insertions(+), 97 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig create mode 100644 src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php create mode 100644 src/Bundle/ChillMainBundle/config/services/mailer.yaml create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220120155303.php rename src/Bundle/ChillPersonBundle/AccompanyingPeriod/{Workflow/WorkflowEventSubscriber.php => Events/UserRefEventSubscriber.php} (53%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 549276554..dc3a8da23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to * [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377) +* [notification: formulaire création] descend la box avec la description dans le bas du formulaire +* [notification for activity]: fix link to activity +* [notification] add "URGENT" before accompanying course with emergency = true +* [notification] add a "read more" button on system notification +* [notification] add `[Chill]` in the subject of each notification, automatically +* [notification] add a counter for notification in activity list and accompanying period list, and search results * [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before. ## Test releases diff --git a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php index 6db1f6945..2947fda38 100644 --- a/src/Bundle/ChillActivityBundle/Controller/ActivityController.php +++ b/src/Bundle/ChillActivityBundle/Controller/ActivityController.php @@ -31,6 +31,7 @@ use DateTime; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Form\Extension\Core\Type\SubmitType; @@ -38,8 +39,8 @@ 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 Symfony\Component\Serializer\SerializerInterface; use function array_key_exists; final class ActivityController extends AbstractController @@ -471,20 +472,21 @@ final class ActivityController extends AbstractController public function showAction(Request $request, int $id): Response { - $view = null; + $entity = $this->activityRepository->find($id); - [$person, $accompanyingPeriod] = $this->getEntity($request); + if (null === $entity) { + throw $this->createNotFoundException('Unable to find Activity entity.'); + } + + $accompanyingPeriod = $entity->getAccompanyingPeriod(); + $person = $entity->getPerson(); if ($accompanyingPeriod instanceof AccompanyingPeriod) { $view = 'ChillActivityBundle:Activity:showAccompanyingCourse.html.twig'; } elseif ($person instanceof Person) { $view = 'ChillActivityBundle:Activity:showPerson.html.twig'; - } - - $entity = $this->activityRepository->find($id); - - if (null === $entity) { - throw $this->createNotFoundException('Unable to find Activity entity.'); + } else { + throw new RuntimeException('the activity should be linked with a period or person'); } if (null !== $accompanyingPeriod) { @@ -493,8 +495,7 @@ final class ActivityController extends AbstractController $entity->personsNotAssociated = $entity->getPersonsNotAssociated(); } - // TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période - // $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity); + $this->denyAccessUnlessGranted(ActivityVoter::SEE, $entity); $deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod); diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index a64142863..6b0639a27 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -1,58 +1,64 @@ {% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %} - {% if no_action is not defined or no_action == false %} -
  • - {{ 'notification.Notify'|trans }} -
  • - {% endif %} - {% if context == 'person' and activity.accompanyingPeriod is not empty %} - {# - Disable person_id in following links, for redirect to accompanyingCourse context - #} - {% set person_id = null %} - {% set accompanying_course_id = activity.accompanyingPeriod.id %} -
  • - - - {{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }} - -
  • - {% endif %} -
  • - -
  • - {% if no_action is not defined or no_action == false %} - {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} + {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} + {% if no_action is not defined or no_action == false %} + {% set notif_counter = chill_count_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) %} + {% if notif_counter.total > 0 %} +
  • {{ chill_counter_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) }}
  • + {% endif %}
  • - + {{ 'notification.Notify'|trans }}
  • {% endif %} - {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %} + {% if context == 'person' and activity.accompanyingPeriod is not empty %} + {# + Disable person_id in following links, for redirect to accompanyingCourse context + #} + {% set person_id = null %} + {% set accompanying_course_id = activity.accompanyingPeriod.id %}
  • - + class="btn btn-primary" + title="{{ 'See activity in accompanying course context'|trans }}"> + + {{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }} +
  • {% endif %} +
  • + +
  • + {% if no_action is not defined or no_action == false %} + {% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %} +
  • + +
  • + {% endif %} + {% if is_granted('CHILL_ACTIVITY_DELETE', activity) %} +
  • + +
  • + {% endif %} + {% endif %} {% endif %} {% endmacro %} diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 772f4d8ee..3f0e2e65b 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -168,6 +168,7 @@ class ChillMainExtension extends Extension implements $loader->load('services/timeline.yaml'); $loader->load('services/search.yaml'); $loader->load('services/serializer.yaml'); + $loader->load('services/mailer.yaml'); $this->configureCruds($container, $config['cruds'], $config['apis'], $loader); } diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php index 0624fff39..b2fe40c60 100644 --- a/src/Bundle/ChillMainBundle/Entity/Notification.php +++ b/src/Bundle/ChillMainBundle/Entity/Notification.php @@ -23,6 +23,9 @@ use Symfony\Component\Validator\Constraints as Assert; * @ORM\Entity * @ORM\Table( * name="chill_main_notification", + * indexes={ + * @ORM\Index(name="chill_main_notification_related_entity_idx", columns={"relatedentityclass", "relatedentityid"}) + * } * ) * @ORM\HasLifecycleCallbacks */ diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php index 658764c26..e04a96af0 100644 --- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php +++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php @@ -50,7 +50,7 @@ class NotificationMailer $email = new TemplatedEmail(); $email ->to($dest->getEmail()) - ->subject('Re: [Chill] ' . $comment->getNotification()->getTitle()) + ->subject('Re: ' . $comment->getNotification()->getTitle()) ->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig') ->context([ 'comment' => $comment, @@ -79,11 +79,13 @@ class NotificationMailer continue; } + $email = new Email(); + $email + ->subject($notification->getTitle()); + if ($notification->isSystem()) { - $email = new Email(); $email - ->text($notification->getMessage()) - ->subject('[Chill] ' . $notification->getTitle()); + ->text($notification->getMessage()); } else { $email = new TemplatedEmail(); $email diff --git a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php index 91bea3197..ed3a8cf66 100644 --- a/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php +++ b/src/Bundle/ChillMainBundle/Notification/NotificationPresence.php @@ -15,12 +15,15 @@ use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; use Chill\MainBundle\Repository\NotificationRepository; use Symfony\Component\Security\Core\Security; +use function array_key_exists; /** * Helps to find if a notification exist for a given entity. */ class NotificationPresence { + private array $cache = []; + private NotificationRepository $notificationRepository; private Security $security; @@ -31,6 +34,29 @@ class NotificationPresence $this->notificationRepository = $notificationRepository; } + public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array + { + if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) { + return $this->cache[$relatedEntityClass][$relatedEntityId]; + } + + $user = $this->security->getUser(); + + if ($user instanceof User) { + $counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated( + $relatedEntityClass, + $relatedEntityId, + $user + ); + + $this->cache[$relatedEntityClass][$relatedEntityId] = $counter; + + return $counter; + } + + return ['unread' => 0, 'read' => 0]; + } + /** * @return array|Notification[] */ diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php index 115adf06b..eb017d912 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtension.php @@ -23,6 +23,13 @@ class NotificationTwigExtension extends AbstractExtension 'needs_environment' => true, 'is_safe' => ['html'], ]), + new TwigFunction('chill_count_notifications', [NotificationTwigExtensionRuntime::class, 'countNotificationsFor'], [ + 'is_safe' => [], + ]), + new TwigFunction('chill_counter_notifications', [NotificationTwigExtensionRuntime::class, 'counterNotificationFor'], [ + 'needs_environment' => true, + 'is_safe' => ['html'], + ]), ]; } } diff --git a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php index e35a39ac0..d5ec75699 100644 --- a/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php +++ b/src/Bundle/ChillMainBundle/Notification/Templating/NotificationTwigExtensionRuntime.php @@ -24,6 +24,21 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface $this->notificationPresence = $notificationPresence; } + public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string + { + return $environment->render( + '@ChillMain/Notification/extension_counter_notifications_for.html.twig', + [ + 'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId), + ] + ); + } + + public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array + { + return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); + } + public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string { $notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId); diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php index 35ec64114..71966c973 100644 --- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php +++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php @@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; +use Doctrine\DBAL\Statement; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityRepository; @@ -24,6 +25,8 @@ final class NotificationRepository implements ObjectRepository { private EntityManagerInterface $em; + private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null; + private EntityRepository $repository; public function __construct(EntityManagerInterface $entityManager) @@ -48,6 +51,30 @@ final class NotificationRepository implements ObjectRepository ->getSingleScalarResult(); } + public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array + { + if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) { + $sql = + 'SELECT + SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread, + SUM((cmn.sender_id = 1812)::int) AS sent, + COUNT(cmn.*) AS total + FROM chill_main_notification cmn + WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL'; + $this->notificationByRelatedEntityAndUserAssociatedStatement = + $this->em->getConnection()->prepare($sql); + } + + $results = $this->notificationByRelatedEntityAndUserAssociatedStatement + ->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]); + + $result = $results->fetchAssociative(); + + $results->free(); + + return $result; + } + public function countUnreadByUser(User $user): int { $sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId'; @@ -153,11 +180,29 @@ final class NotificationRepository implements ObjectRepository * @return array|Notification[] */ public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array + { + return + $this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user) + ->select('n') + ->getQuery() + ->getResult(); + } + + public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification + { + return $this->repository->findOneBy($criteria, $orderBy); + } + + public function getClassName() + { + return Notification::class; + } + + private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder { $qb = $this->repository->createQueryBuilder('n'); $qb - ->select('n') ->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass')) ->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId')) ->andWhere($qb->expr()->isNotNull('n.sender')) @@ -171,17 +216,7 @@ final class NotificationRepository implements ObjectRepository ->setParameter('relatedEntityId', $relatedEntityId) ->setParameter('user', $user); - return $qb->getQuery()->getResult(); - } - - public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification - { - return $this->repository->findOneBy($criteria, $orderBy); - } - - public function getClassName() - { - return Notification::class; + return $qb; } private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig index 91de10f99..391158a00 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig @@ -62,6 +62,7 @@ {{ c.notification.message|chill_markdown_to_html }} {% else %} {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} +

    {{ 'Read more'|trans }}

    {% endif %} @@ -85,7 +86,13 @@ {% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
  • + class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}"> + {% if not c.notification.isSystem() %} + + {% else %} + {{ 'Read more'|trans }} + {% endif %} +
  • {% endif %}
diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig index 8797c276a..ce5934e52 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/create.html.twig @@ -21,8 +21,6 @@ {{ form_row(form.title, { 'label': 'notification.subject'|trans }) }} {{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }} - {% include handler.template(notification) with handler.templateData(notification) %} -
@@ -30,6 +28,8 @@
+ {% include handler.template(notification) with handler.templateData(notification) %} + {{ form_end(form) }}
    diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig new file mode 100644 index 000000000..2d8b06ae1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_counter_notifications_for.html.twig @@ -0,0 +1,2 @@ +{% if counter.total > 0 %}{{ 'notification.counter total notifications'|trans({'total': counter.total }) }}{% endif %} +{% if counter.unread > 0 %}{{ 'notification.counter unread notifications'|trans({'unread': counter.unread })}}{% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig index 8bb695078..ac1daad6f 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/extension_list_notifications_for.html.twig @@ -18,7 +18,7 @@ data-notification-id="{{ notification.id }}" data-notification-current-is-read="{{ notification.isReadBy(app.user) }}" data-container="notification-status" - data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}" + data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}, false) }}" data-button-class="btn-outline-primary" data-button-text="false" > diff --git a/src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php b/src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php new file mode 100644 index 000000000..9e1648753 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php @@ -0,0 +1,50 @@ +initial = $initial; + $this->chillLogger = $chillLogger; + } + + public function send(RawMessage $message, ?Envelope $envelope = null): void + { + if ($message instanceof Email) { + $message->subject($this->prefix . $message->getSubject()); + } + + $this->chillLogger->info('chill email sent', [ + 'to' => array_map(static function (Address $address) { + return $address->getAddress(); + }, $message->getTo()), + 'subject' => $message->getSubject(), + ]); + + $this->initial->send($message, $envelope); + } +} diff --git a/src/Bundle/ChillMainBundle/config/services/mailer.yaml b/src/Bundle/ChillMainBundle/config/services/mailer.yaml new file mode 100644 index 000000000..d7305f4b1 --- /dev/null +++ b/src/Bundle/ChillMainBundle/config/services/mailer.yaml @@ -0,0 +1,10 @@ +services: + _defaults: + autowire: true + autoconfigure: true + + Chill\MainBundle\Service\Mailer\: + resource: '../../Service/Mailer' + + Chill\MainBundle\Service\Mailer\ChillMailer: + decorates: Symfony\Component\Mailer\MailerInterface diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220120155303.php b/src/Bundle/ChillMainBundle/migrations/Version20220120155303.php new file mode 100644 index 000000000..617d24b27 --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220120155303.php @@ -0,0 +1,33 @@ +addSql('DROP INDEX chill_main_notification_related_entity_idx'); + } + + public function getDescription(): string + { + return 'Create index for counting notifications'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE INDEX chill_main_notification_related_entity_idx ON chill_main_notification (relatedentityclass, relatedentityid DESC)'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml index 4f25b46da..7a5b328f4 100644 --- a/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillMainBundle/translations/messages+intl-icu.fr.yaml @@ -13,3 +13,19 @@ notification: few {# notifications} other {# notifications} } + + counter total notifications: >- + {total, plural, + =0 {Aucune notification} + one {# notification} + few {# notifications} + other {# notifications} + } + + counter unread notifications: >- + {unread, plural, + =0 {Aucune non-lue} + one {# non-lue} + few {# non-lues} + other {# non-lues} + } diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index af0dc77d7..434909d9f 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -58,6 +58,7 @@ comment: commentaire Comment: Commentaire Pinned comment: Commentaire épinglé Any comment: Aucun commentaire +Read more: Lire la suite # comment embeddable No comment associated: Aucun commentaire diff --git a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Workflow/WorkflowEventSubscriber.php b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php similarity index 53% rename from src/Bundle/ChillPersonBundle/AccompanyingPeriod/Workflow/WorkflowEventSubscriber.php rename to src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php index 3b7626903..590874ab4 100644 --- a/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Workflow/WorkflowEventSubscriber.php +++ b/src/Bundle/ChillPersonBundle/AccompanyingPeriod/Events/UserRefEventSubscriber.php @@ -9,19 +9,20 @@ declare(strict_types=1); -namespace Chill\PersonBundle\AccompanyingPeriod\Workflow; +namespace Chill\PersonBundle\AccompanyingPeriod\Events; use Chill\MainBundle\Entity\Notification; use Chill\MainBundle\Entity\User; use Chill\PersonBundle\Entity\AccompanyingPeriod; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\Event\LifecycleEventArgs; use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Security\Core\Security; use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Workflow\Event\EnteredEvent; use Symfony\Contracts\Translation\TranslatorInterface; -class WorkflowEventSubscriber implements EventSubscriberInterface +class UserRefEventSubscriber implements EventSubscriberInterface { private EntityManagerInterface $em; @@ -55,23 +56,46 @@ class WorkflowEventSubscriber implements EventSubscriberInterface } } + public function postUpdate(AccompanyingPeriod $period, LifecycleEventArgs $args): void + { + if ($period->hasPreviousUser() + && $period->getUser() !== $this->security->getUser() + && $period->getStep() !== AccompanyingPeriod::STEP_DRAFT + ) { + $this->generateNotificationToUser($period); + } + + // we are just out of a flush operation. Launch a new one + $this->em->flush(); + } + + private function generateNotificationToUser(AccompanyingPeriod $period) + { + $notification = new Notification(); + + $urgentStatement = + $period->isEmergency() ? strtoupper($this->translator->trans('accompanying_period.emergency')) . ' ' : ''; + + $notification + ->setRelatedEntityId($period->getId()) + ->setRelatedEntityClass(AccompanyingPeriod::class) + ->setTitle($urgentStatement . $this->translator->trans('period_notification.period_designated_subject')) + ->setMessage($this->engine->render( + '@ChillPerson/Notification/accompanying_course_designation.md.twig', + [ + 'accompanyingCourse' => $period, + ] + )) + ->addAddressee($period->getUser()); + + $this->em->persist($notification); + } + private function onPeriodConfirmed(AccompanyingPeriod $period) { if ($period->getUser() instanceof User && $period->getUser() !== $this->security->getUser()) { - $notification = new Notification(); - $notification - ->setRelatedEntityId($period->getId()) - ->setRelatedEntityClass(AccompanyingPeriod::class) - ->setTitle($this->translator->trans('period_notification.period_designated_subject')) - ->setMessage($this->engine->render( - '@ChillPerson/Notification/accompanying_course_designation.md.twig', - [ - 'accompanyingCourse' => $period, - ] - )) - ->addAddressee($period->getUser()); - $this->em->persist($notification); + $this->generateNotificationToUser($period); } } } diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 06e4f0c8b..daf68098d 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -327,6 +327,13 @@ class AccompanyingPeriod implements */ private ?User $user = null; + /** + * Temporary field, which is filled when the user is changed. + * + * Used internally for listener when user change + */ + private ?User $userPrevious = null; + /** * @ORM\OneToMany( * targetEntity=AccompanyingPeriodWork::class, @@ -755,6 +762,11 @@ class AccompanyingPeriod implements return $this->pinnedComment; } + public function getPreviousUser(): ?User + { + return $this->userPrevious; + } + /** * @return Collection|SocialAction[] All the descendant social actions of all * the descendants of the entity @@ -868,6 +880,11 @@ class AccompanyingPeriod implements return $this->works; } + public function hasPreviousUser(): bool + { + return null !== $this->userPrevious; + } + /** * Returns true if the closing date is after the opening date. */ @@ -1172,6 +1189,10 @@ class AccompanyingPeriod implements public function setUser(User $user): self { + if ($this->user !== $user) { + $this->userPrevious = $this->user; + } + $this->user = $user; return $this; diff --git a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php index 82437eb30..a33858c02 100644 --- a/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php +++ b/src/Bundle/ChillPersonBundle/Form/CreationPersonType.php @@ -61,13 +61,13 @@ final class CreationPersonType extends AbstractType 'required' => false, ]) ->add('phonenumber', TelType::class, [ - 'required' => false + 'required' => false, ]) ->add('mobilenumber', TelType::class, [ - 'required' => false + 'required' => false, ]) ->add('email', EmailType::class, [ - 'required' => false + 'required' => false, ]); if ($this->askCenters) { diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig index 83a047c98..b97847e90 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/index.html.twig @@ -131,6 +131,10 @@ {{ 'le ' ~ w.updatedAt|format_datetime('long', 'short') }}
      + {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) %} + {% if notif_counter.total > 0 %} +
    • {{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod\\AccompanyingPeriodWork', w.id) }}
    • + {% endif %}
    • 0 %} +
    • {{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', period.id) }}
    • + {% endif %}
    • {# {{ 'See this period'|trans }} #} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig index 96b7b9eef..891e19b8b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Person/list_with_period.html.twig @@ -118,6 +118,10 @@ {{ 'File number'|trans }} {{ acp.id }} + {% set notif_counter = chill_count_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) %} + {% if notif_counter.total > 0 %} +
      {{ chill_counter_notifications('Chill\\PersonBundle\\Entity\\AccompanyingPeriod', acp.id) }}
      + {% endif %}
      diff --git a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml index 4dbdd43cb..7d7f9072d 100644 --- a/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/accompanyingPeriod.yaml @@ -20,9 +20,19 @@ services: Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestionInterface: '@Chill\PersonBundle\AccompanyingPeriod\Suggestion\ReferralsSuggestion' - Chill\PersonBundle\AccompanyingPeriod\Workflow\: - resource: './../../AccompanyingPeriod/Workflow' + Chill\PersonBundle\AccompanyingPeriod\Events\: + resource: './../../AccompanyingPeriod/Events' autowire: true autoconfigure: true + Chill\PersonBundle\AccompanyingPeriod\Events\UserRefEventSubscriber: + autowire: true + autoconfigure: true + tags: + - # these are the options required to define the entity listener + name: 'doctrine.orm.entity_listener' + event: 'postUpdate' + entity: 'Chill\PersonBundle\Entity\AccompanyingPeriod' + # set the 'lazy' option to TRUE to only instantiate listeners when they are used + lazy: true From 53b3f98bba35d4dfef192ebb8d6becd0e13aaa78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Mon, 24 Jan 2022 10:59:00 +0000 Subject: [PATCH 19/21] Household/composition add + fixes household composition editor --- .../Doctrine/Model/TrackCreationTrait.php | 56 ++++++ .../Doctrine/Model/TrackUpdateTrait.php | 56 ++++++ .../HouseholdCompositionController.php | 149 +++++++++++++++ .../HouseholdCompositionTypeApiController.php | 36 ++++ .../ORM/LoadHouseholdCompositionType.php | 48 +++++ .../ChillPersonExtension.php | 16 ++ .../Entity/Household/Household.php | 104 ++++++++++- .../Entity/Household/HouseholdComposition.php | 171 ++++++++++++++++++ .../Household/HouseholdCompositionType.php | 76 ++++++++ .../ChillPersonBundle/Entity/Person.php | 2 +- .../Form/HouseholdCompositionType.php | 60 ++++++ .../Household/MembersEditor.php | 13 +- .../Menu/HouseholdMenuBuilder.php | 22 ++- .../Household/HouseholdACLAwareRepository.php | 2 +- .../HouseholdCompositionRepository.php | 75 ++++++++ .../HouseholdCompositionTypeRepository.php | 69 +++++++ .../vuejs/HouseholdMembersEditor/App.vue | 9 +- .../components/Concerned.vue | 31 ++-- .../components/Dates.vue | 50 ++++- .../components/PersonComment.vue | 42 +++++ .../components/Positioning.vue | 14 +- .../vuejs/HouseholdMembersEditor/js/i18n.js | 8 +- .../HouseholdMembersEditor/store/index.js | 34 +++- .../_warning_address.html.twig | 38 ++-- .../views/Household/members_editor.html.twig | 10 +- .../views/Household/summary.html.twig | 52 +++++- .../HouseholdComposition/index.html.twig | 96 ++++++++++ .../Security/Authorization/HouseholdVoter.php | 63 ++++++- .../Normalizer/MembersEditorNormalizer.php | 20 ++ .../HouseholdMemberControllerTest.php | 3 + .../Tests/Entity/Household/HouseholdTest.php | 47 +++++ .../config/services/security.yaml | 4 + .../migrations/Version20220121121310.php | 51 ++++++ .../translations/messages+intl-icu.fr.yaml | 14 ++ .../translations/messages.fr.yml | 11 ++ 35 files changed, 1489 insertions(+), 63 deletions(-) create mode 100644 src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationTrait.php create mode 100644 src/Bundle/ChillMainBundle/Doctrine/Model/TrackUpdateTrait.php create mode 100644 src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionController.php create mode 100644 src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionTypeApiController.php create mode 100644 src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHouseholdCompositionType.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Household/HouseholdComposition.php create mode 100644 src/Bundle/ChillPersonBundle/Entity/Household/HouseholdCompositionType.php create mode 100644 src/Bundle/ChillPersonBundle/Form/HouseholdCompositionType.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionTypeRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/PersonComment.vue create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/HouseholdComposition/index.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdTest.php create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20220121121310.php diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationTrait.php b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationTrait.php new file mode 100644 index 000000000..a991399c8 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackCreationTrait.php @@ -0,0 +1,56 @@ +createdAt; + } + + public function getCreatedBy(): ?User + { + return $this->createdBy; + } + + public function setCreatedAt(DateTimeInterface $datetime): self + { + $this->createdAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime; + + return $this; + } + + public function setCreatedBy(User $user): self + { + $this->createdBy = $user; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Doctrine/Model/TrackUpdateTrait.php b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackUpdateTrait.php new file mode 100644 index 000000000..3c706459c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Doctrine/Model/TrackUpdateTrait.php @@ -0,0 +1,56 @@ +updatedAt; + } + + public function getUpdatedBy(): ?User + { + return $this->updatedBy; + } + + public function setUpdatedAt(DateTimeInterface $datetime): self + { + $this->updatedAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime; + + return $this; + } + + public function setUpdatedBy(User $user): self + { + $this->updatedBy = $user; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionController.php new file mode 100644 index 000000000..c74113a05 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionController.php @@ -0,0 +1,149 @@ +security = $security; + $this->householdCompositionRepository = $householdCompositionRepository; + $this->paginatorFactory = $paginatorFactory; + $this->formFactory = $formFactory; + $this->entityManager = $entityManager; + $this->translator = $translator; + $this->engine = $engine; + $this->urlGenerator = $urlGenerator; + } + + /** + * @Route("/{_locale}/person/household/{id}/composition/index", name="chill_person_household_composition_index") + */ + public function index(Household $household, Request $request): Response + { + if (!$this->security->isGranted(HouseholdVoter::SEE, $household)) { + throw new AccessDeniedException('not allowed to edit an household'); + } + + $count = $this->householdCompositionRepository->countByHousehold($household); + $paginator = $this->paginatorFactory->create($count); + $compositions = $this->householdCompositionRepository->findByHousehold( + $household, + ['startDate' => 'DESC', 'id' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + if ($this->security->isGranted(HouseholdVoter::EDIT, $household)) { + $isEdit = $request->query->has('edit'); + + if ($isEdit) { + $householdCompositions = $household->getCompositions()->filter(static function (HouseholdComposition $composition) use ($request) { + return $composition->getId() === $request->query->getInt('edit'); + }); + + if ($householdCompositions->count() !== 1) { + throw new BadRequestHttpException('could not find the composition with this id associated to the household'); + } + $householdComposition = $householdCompositions->first(); + } else { + $householdComposition = (new HouseholdComposition()) + ->setStartDate(new DateTimeImmutable()); + } + $form = $this->formFactory->create(HouseholdCompositionType::class, $householdComposition); + + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + if (!$isEdit) { + $this->entityManager->persist($householdComposition); + $household->addComposition($householdComposition); + } + + $this->entityManager->flush(); + + $request->getSession()->getFlashBag()->add( + 'success', + $this->translator->trans('household_composition.Composition added') + ); + + return new RedirectResponse( + $this->urlGenerator->generate('chill_person_household_composition_index', [ + 'id' => $household->getId(), + ]) + ); + } + + if ($form->isSubmitted() && !$form->isValid()) { + $request->getSession()->getFlashBag()->add( + 'warning', + $this->translator->trans('This form contains errors') + ); + } + } + + return new Response($this->engine->render( + '@ChillPerson/HouseholdComposition/index.html.twig', + [ + 'household' => $household, + 'compositions' => $compositions, + 'form' => isset($form) ? $form->createView() : null, + 'isPosted' => isset($form) ? $form->isSubmitted() : false, + 'editId' => $request->query->getInt('edit', -1), + ] + )); + } +} diff --git a/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionTypeApiController.php b/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionTypeApiController.php new file mode 100644 index 000000000..418d04531 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Controller/HouseholdCompositionTypeApiController.php @@ -0,0 +1,36 @@ +andWhere($query->expr()->eq('e.active', "'TRUE'")); + + break; + + default: + throw new UnexpectedValueException('unexepcted action: ' . $action); + } + } +} diff --git a/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHouseholdCompositionType.php b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHouseholdCompositionType.php new file mode 100644 index 000000000..b1d306ef8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/DataFixtures/ORM/LoadHouseholdCompositionType.php @@ -0,0 +1,48 @@ + 'Couple avec enfant(s)'], + ['fr' => 'Couple sans enfant'], + ['fr' => 'Mère seule'], + ['fr' => 'Père seul'], + ['fr' => 'Mère isolée'], + ['fr' => 'Père isolé'], + ['fr' => 'Homme seul'], + ['fr' => 'Femme seule'], + ]; + + public static function getGroups(): array + { + return ['composition-type']; + } + + public function load(ObjectManager $manager) + { + foreach (self::TYPES as $type) { + $manager->persist( + (new HouseholdCompositionType()) + ->setLabel($type) + ); + } + + $manager->flush(); + } +} diff --git a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php index 5982fcebe..01df460e3 100644 --- a/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php +++ b/src/Bundle/ChillPersonBundle/DependencyInjection/ChillPersonExtension.php @@ -13,6 +13,7 @@ namespace Chill\PersonBundle\DependencyInjection; use Chill\MainBundle\DependencyInjection\MissingBundleException; use Chill\MainBundle\Security\Authorization\ChillExportVoter; +use Chill\PersonBundle\Controller\HouseholdCompositionTypeApiController; use Chill\PersonBundle\Doctrine\DQL\AddressPart; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodResourceVoter; use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter; @@ -760,6 +761,21 @@ class ChillPersonExtension extends Extension implements PrependExtensionInterfac ], ], ], + [ + 'class' => \Chill\PersonBundle\Entity\Household\HouseholdCompositionType::class, + 'name' => 'household_composition', + 'base_path' => '/api/1.0/person/houehold/composition/type', + 'base_role' => 'ROLE_USER', + 'controller' => HouseholdCompositionTypeApiController::class, + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + ], + ], ], ]); } diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php index 865526250..ef718bc2e 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Household/Household.php +++ b/src/Bundle/ChillPersonBundle/Entity/Household/Household.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace Chill\PersonBundle\Entity\Household; +use ArrayIterator; use Chill\MainBundle\Entity\Address; use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable; use Chill\PersonBundle\Validator\Constraints\Household\MaxHolder; @@ -23,8 +24,8 @@ use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Validator\Constraints as Assert; -use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; use function count; /** @@ -56,6 +57,18 @@ class Household */ private CommentEmbeddable $commentMembers; + /** + * @ORM\OneToMany( + * targetEntity=HouseholdComposition::class, + * mappedBy="household", + * orphanRemoval=true, + * cascade={"persist"} + * ) + * @ORM\OrderBy({"startDate": "DESC"}) + * @Assert\Valid(traverse=true, groups={"household_composition"}) + */ + private Collection $compositions; + /** * @ORM\Id * @ORM\GeneratedValue @@ -90,6 +103,7 @@ class Household $this->addresses = new ArrayCollection(); $this->members = new ArrayCollection(); $this->commentMembers = new CommentEmbeddable(); + $this->compositions = new ArrayCollection(); } /** @@ -108,6 +122,18 @@ class Household return $this; } + public function addComposition(HouseholdComposition $composition): self + { + if (!$this->compositions->contains($composition)) { + $composition->setHousehold($this); + $this->compositions[] = $composition; + } + + $this->householdCompositionConsistency(); + + return $this; + } + public function addMember(HouseholdMember $member): self { if (!$this->members->contains($member)) { @@ -136,6 +162,14 @@ class Household return $this->commentMembers; } + /** + * @return ArrayCollection|Collection|HouseholdComposition[] + */ + public function getCompositions(): Collection + { + return $this->compositions; + } + /** * @Serializer\Groups({"read", "docgen:read"}) * @Serializer\SerializedName("current_address") @@ -157,6 +191,31 @@ class Household return null; } + public function getCurrentComposition(?DateTimeImmutable $at = null): ?HouseholdComposition + { + $at ??= new DateTimeImmutable('today'); + $criteria = new Criteria(); + $expr = Criteria::expr(); + + $criteria->where( + $expr->andX( + $expr->orX( + $expr->isNull('endDate'), + $expr->gt('endDate', $at) + ), + $expr->lte('startDate', $at) + ) + ); + + $compositions = $this->compositions->matching($criteria); + + if ($compositions->count() > 0) { + return $compositions->first(); + } + + return null; + } + /** * @Serializer\Groups({"docgen:read"}) */ @@ -369,11 +428,54 @@ class Household return $this->waitingForBirthDate; } + /** + * @internal + */ + public function householdCompositionConsistency(): void + { + $compositionOrdered = $this->compositions->toArray(); + + usort( + $compositionOrdered, + static function (HouseholdComposition $a, HouseholdComposition $b) { + return $a->getStartDate() <=> $b->getStartDate(); + } + ); + + $iterator = new ArrayIterator($compositionOrdered); + $iterator->rewind(); + + /** @var ?HouseholdComposition $previous */ + $previous = null; + + do { + /** @var ?HouseholdComposition $current */ + $current = $iterator->current(); + + if (null !== $previous) { + if (null === $previous->getEndDate() || $previous->getEndDate() > $current->getStartDate()) { + $previous->setEndDate($current->getStartDate()); + } + } + $previous = $current; + $iterator->next(); + } while ($iterator->valid()); + } + public function removeAddress(Address $address) { $this->addresses->removeElement($address); } + public function removeComposition(HouseholdComposition $composition): self + { + if ($this->compositions->removeElement($composition)) { + $composition->setHousehold(null); + } + + return $this; + } + public function removeMember(HouseholdMember $member): self { if ($this->members->removeElement($member)) { diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdComposition.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdComposition.php new file mode 100644 index 000000000..a381fafd8 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdComposition.php @@ -0,0 +1,171 @@ +comment = new CommentEmbeddable(); + } + + public function getComment(): CommentEmbeddable + { + return $this->comment; + } + + public function getEndDate(): ?DateTimeImmutable + { + return $this->endDate; + } + + public function getHousehold(): ?Household + { + return $this->household; + } + + public function getHouseholdCompositionType(): ?HouseholdCompositionType + { + return $this->householdCompositionType; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getNumberOfChildren(): ?int + { + return $this->numberOfChildren; + } + + public function getStartDate(): ?DateTimeImmutable + { + return $this->startDate; + } + + public function setComment(CommentEmbeddable $comment): HouseholdComposition + { + $this->comment = $comment; + + return $this; + } + + public function setEndDate(?DateTimeImmutable $endDate): HouseholdComposition + { + $this->endDate = $endDate; + + if (null !== $this->household) { + $this->household->householdCompositionConsistency(); + } + + return $this; + } + + public function setHousehold(?Household $household): HouseholdComposition + { + $this->household = $household; + + return $this; + } + + public function setHouseholdCompositionType(?HouseholdCompositionType $householdCompositionType): HouseholdComposition + { + $this->householdCompositionType = $householdCompositionType; + + return $this; + } + + public function setNumberOfChildren(?int $numberOfChildren): HouseholdComposition + { + $this->numberOfChildren = $numberOfChildren; + + return $this; + } + + public function setStartDate(?DateTimeImmutable $startDate): HouseholdComposition + { + $this->startDate = $startDate; + + if (null !== $this->household) { + $this->household->householdCompositionConsistency(); + } + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdCompositionType.php b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdCompositionType.php new file mode 100644 index 000000000..03d1a401a --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Entity/Household/HouseholdCompositionType.php @@ -0,0 +1,76 @@ +id; + } + + public function getLabel(): array + { + return $this->label; + } + + public function isActive(): bool + { + return $this->active; + } + + public function setActive(bool $active): HouseholdCompositionType + { + $this->active = $active; + + return $this; + } + + public function setLabel(array $label): HouseholdCompositionType + { + $this->label = $label; + + return $this; + } +} diff --git a/src/Bundle/ChillPersonBundle/Entity/Person.php b/src/Bundle/ChillPersonBundle/Entity/Person.php index 91e430a2f..e2847c73c 100644 --- a/src/Bundle/ChillPersonBundle/Entity/Person.php +++ b/src/Bundle/ChillPersonBundle/Entity/Person.php @@ -1142,7 +1142,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI ->where( $expr->eq('shareHousehold', true) ) - ->orderBy(['startDate' => Criteria::DESC]); + ->orderBy(['startDate' => Criteria::DESC, 'id' => Criteria::DESC]); return $this->getHouseholdParticipations() ->matching($criteria); diff --git a/src/Bundle/ChillPersonBundle/Form/HouseholdCompositionType.php b/src/Bundle/ChillPersonBundle/Form/HouseholdCompositionType.php new file mode 100644 index 000000000..0348b0c39 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Form/HouseholdCompositionType.php @@ -0,0 +1,60 @@ +householdCompositionTypeRepository = $householdCompositionTypeRepository; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $types = $this->householdCompositionTypeRepository->findAllActive(); + + $builder + ->add('householdCompositionType', EntityType::class, [ + 'class' => \Chill\PersonBundle\Entity\Household\HouseholdCompositionType::class, + 'choices' => $types, + 'choice_label' => function (\Chill\PersonBundle\Entity\Household\HouseholdCompositionType $type) { + return $this->translatableStringHelper->localize($type->getLabel()); + }, + 'label' => 'household_composition.Household composition', + ]) + ->add('startDate', ChillDateType::class, [ + 'required' => true, + 'input' => 'datetime_immutable', + ]) + ->add('numberOfChildren', IntegerType::class, [ + 'required' => true, + 'label' => 'household_composition.numberOfChildren', + ]) + ->add('comment', CommentType::class, [ + 'required' => false, + ]); + } +} diff --git a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php index 4d855a980..adc97a4ac 100644 --- a/src/Bundle/ChillPersonBundle/Household/MembersEditor.php +++ b/src/Bundle/ChillPersonBundle/Household/MembersEditor.php @@ -29,6 +29,8 @@ class MembersEditor { public const VALIDATION_GROUP_AFFECTED = 'household_memberships'; + public const VALIDATION_GROUP_COMPOSITION = 'household_composition'; + public const VALIDATION_GROUP_CREATED = 'household_memberships_created'; private ?Household $household = null; @@ -77,6 +79,15 @@ class MembersEditor $this->oldMembershipsHashes[] = spl_object_hash($participation); } } + + foreach ($person->getHouseholdParticipationsNotShareHousehold() as $participation) { + if ($participation->getHousehold() === $this->household + && $participation->getEndDate() === null || $participation->getEndDate() > $membership->getStartDate() + && $participation->getStartDate() <= $membership->getStartDate() + ) { + $participation->setEndDate($membership->getStartDate()); + } + } } $this->membershipsAffected[] = $membership; @@ -129,7 +140,7 @@ class MembersEditor { if ($this->hasHousehold()) { $list = $this->validator - ->validate($this->getHousehold(), null, [self::VALIDATION_GROUP_AFFECTED]); + ->validate($this->getHousehold(), null, [self::VALIDATION_GROUP_AFFECTED, self::VALIDATION_GROUP_COMPOSITION]); } else { $list = new ConstraintViolationList(); } diff --git a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php index a144bf65b..a4d3f8d03 100644 --- a/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php +++ b/src/Bundle/ChillPersonBundle/Menu/HouseholdMenuBuilder.php @@ -29,6 +29,7 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface public function buildMenu($menuId, MenuItem $menu, array $parameters): void { + /** @var \Chill\PersonBundle\Entity\Household\Household $household */ $household = $parameters['household']; $menu->addChild($this->translator->trans('household.Household summary'), [ @@ -38,6 +39,20 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface ], ]) ->setExtras(['order' => 10]); + $menu->addChild($this->translator->trans('household.Relationship'), [ + 'route' => 'chill_person_household_relationship', + 'routeParameters' => [ + 'household_id' => $household->getId(), + ], ]) + ->setExtras(['order' => 15]); + + $menu->addChild($this->translator->trans('household_composition.Compositions'), [ + 'route' => 'chill_person_household_composition_index', + 'routeParameters' => [ + 'id' => $household->getId(), + ], ]) + ->setExtras(['order' => 17]); + $menu->addChild($this->translator->trans('household.Accompanying period'), [ 'route' => 'chill_person_household_accompanying_period', 'routeParameters' => [ @@ -51,13 +66,6 @@ class HouseholdMenuBuilder implements LocalMenuBuilderInterface 'household_id' => $household->getId(), ], ]) ->setExtras(['order' => 30]); - - $menu->addChild($this->translator->trans('household.Relationship'), [ - 'route' => 'chill_person_household_relationship', - 'routeParameters' => [ - 'household_id' => $household->getId(), - ], ]) - ->setExtras(['order' => 15]); } public static function getMenuIds(): array diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php index 0649dae1c..dc5e8f5d3 100644 --- a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdACLAwareRepository.php @@ -39,7 +39,7 @@ final class HouseholdACLAwareRepository implements HouseholdACLAwareRepositoryIn { $centers = $this->authorizationHelper->getReachableCenters( $this->security->getUser(), - HouseholdVoter::SHOW + HouseholdVoter::SEE ); if ([] === $centers) { diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php new file mode 100644 index 000000000..23f79c7f5 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionRepository.php @@ -0,0 +1,75 @@ +repository = $entityManager->getRepository(HouseholdComposition::class); + } + + public function countByHousehold(Household $household): int + { + return $this->repository->count(['household' => $household]); + } + + public function find($id): ?HouseholdComposition + { + return $this->repository->find($id); + } + + /** + * @return array|HouseholdComposition[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @param int $limit + * @param int $offset + * + * @return array|object[]|HouseholdComposition[] + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + /** + * @return array|HouseholdComposition[]|object[] + */ + public function findByHousehold(Household $household, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array + { + return $this->findBy(['household' => $household], $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?HouseholdComposition + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return HouseholdComposition::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionTypeRepository.php b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionTypeRepository.php new file mode 100644 index 000000000..45931b4da --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/Household/HouseholdCompositionTypeRepository.php @@ -0,0 +1,69 @@ +repository = $entityManager->getRepository(HouseholdCompositionType::class); + } + + public function find($id): ?HouseholdCompositionType + { + return $this->repository->find($id); + } + + /** + * @return array|HouseholdCompositionType[]|object[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @return array|HouseholdCompositionType[] + */ + public function findAllActive(): array + { + return $this->findBy(['active' => true]); + } + + /** + * @param $limit + * @param $offset + * + * @return array|HouseholdCompositionType[]|object[] + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?HouseholdCompositionType + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return HouseholdCompositionType::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue index 22b4f54a6..52047c630 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/App.vue @@ -32,7 +32,7 @@
    • -
    • @@ -104,6 +104,13 @@ export default { return false; }, + lastStepIsSaveAllowed() { + let r = !this.$store.getters.isHouseholdNew || + (this.$store.state.numberOfChildren !== null && this.$store.state.householdCompositionType !== null); + console.log('is saved allowed ?', r); + + return r; + }, }, methods: { goToNext() { 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 3be8822be..74271954f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Concerned.vue @@ -7,18 +7,14 @@
      -

      - {{ $t('household_members_editor.concerned.persons_will_be_moved') }} : - - - - -

      +

      {{ $t('household_members_editor.concerned.persons_will_be_moved') }} :

      + +
        +
      • + {{ c.person.text }} +
      • +
      +

      {{ $t('household_members_editor.concerned.persons_with_household') }}

        @@ -108,9 +104,14 @@ export default { this.$refs.addPersons.resetSearch(); // to cast child method modal.showModal = false; }, - removePerson(person) { - console.log('remove person in concerned', person); - this.$store.dispatch('removePerson', person); + removeConcerned(concerned) { + console.log('removedconcerned', concerned); + + if (!concerned.allowRemove) { + return; + } + + this.$store.dispatch('removePerson', concerned.person); }, makeHouseholdLink(id) { return `/fr/person/household/${id}/summary` diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue index c40cc9cbc..94c1a146a 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Dates.vue @@ -4,17 +4,38 @@

        {{ $t('household_members_editor.dates.dates_title') }}

        -

        -

        + - -

        +
        + +
        +
        + +
        +

        {{ $t('household_members_editor.composition.composition') }}

        +
        + +
        + +
        +
        +
        + +
        + +
        +
        +
        + + diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Positioning.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Positioning.vue index 8e033234c..487fb230e 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Positioning.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/components/Positioning.vue @@ -3,15 +3,15 @@

        {{ $t('household_members_editor.positioning.persons_to_positionnate')}}

        -
        +
        -
        +
        - +

        {{ conc.person.text }}

        +
        +
        +
        {{ $t('household_members_editor.positioning.comment') }}
        + +
        +
        @@ -46,12 +52,14 @@ import MemberDetails from './MemberDetails.vue'; import {mapGetters, mapState} from "vuex"; import CurrentHousehold from "./CurrentHousehold"; import PersonRenderBox from 'ChillPersonAssets/vuejs/_components/Entity/PersonRenderBox.vue'; +import PersonComment from './PersonComment'; export default { name: "Positioning", components: { CurrentHousehold, PersonRenderBox, + PersonComment, }, computed: { ...mapState([ 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 e09fddf0f..66246058f 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/js/i18n.js @@ -52,6 +52,7 @@ const appMessages = { positioning: { persons_to_positionnate: 'Usagers à positionner', holder: "Titulaire", + comment: "Commentaire", }, app: { next: 'Suivant', @@ -77,7 +78,12 @@ const appMessages = { dates: { start_date: "Début de validité", end_date: "Fin de validité", - dates_title: "Période de validité", + dates_title: "Depuis le", + }, + composition: { + composition: "Composition familiale", + household_composition: "Composition du ménage", + number_of_children: "Nombre d'enfants mineurs au sein du ménage", }, confirmation: { save: "Enregistrer", 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 cd43ee29c..cc5ab497c 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/HouseholdMembersEditor/store/index.js @@ -1,5 +1,6 @@ import { createStore } from 'vuex'; import { householdMove, fetchHouseholdSuggestionByAccompanyingPeriod, fetchAddressSuggestionByPerson} from './../api.js'; +import { fetchResults } from 'ChillMainAssets/lib/api/apiMethods.js' import { fetchHouseholdByAddressReference } from 'ChillPersonAssets/lib/household.js'; import { datetimeToISO } from 'ChillMainAssets/chill/js/date.js'; @@ -54,8 +55,11 @@ const store = createStore({ */ householdSuggestionByAccompanyingPeriod: [], // TODO rename into householdsSuggestion showHouseholdSuggestion: window.household_members_editor_expand_suggestions === 1, + householdCompositionType: null, + numberOfChildren: 0, addressesSuggestion: [], showAddressSuggestion: true, + householdCompositionTypes: [], warnings: [], errors: [] }, @@ -250,7 +254,8 @@ const store = createStore({ payload_conc, payload = { concerned: [], - destination: null + destination: null, + composition: null, } ; @@ -261,7 +266,6 @@ const store = createStore({ }; if (getters.isHouseholdNew && state.household.current_address !== null) { - console.log(state.household); payload.destination.forceAddress = { id: state.household.current_address.address_id }; } } @@ -290,6 +294,19 @@ const store = createStore({ payload.concerned.push(payload_conc); } + if (getters.isHouseholdNew) { + payload.composition = { + household_composition_type: { + type: state.householdCompositionType.type, + id: state.householdCompositionType.id, + }, + number_of_children: state.numberOfChildren, + start_date: { + datetime: datetimeToISO(state.startDate), + }, + }; + } + return payload; }, }, @@ -409,6 +426,15 @@ const store = createStore({ setErrors(state, errors) { state.errors = errors; }, + setHouseholdCompositionTypes(state, types) { + state.householdCompositionTypes = types; + }, + setHouseholdCompositionType(state, id) { + state.householdCompositionType = state.householdCompositionTypes.find(t => t.id = id); + }, + setNumberOfChildren(state, number) { + state.numberOfChildren = Number.parseInt(number); + }, addAddressesSuggestion(state, addresses) { let existingIds = state.addressesSuggestion .map(a => a.address_id); @@ -570,4 +596,8 @@ if (concerned.length > 0) { }); } +fetchResults(`/api/1.0/person/houehold/composition/type.json`).then(types => { + store.commit('setHouseholdCompositionTypes', types); +}) + export { store }; diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig index 2febc7967..1f9c09845 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourse/_warning_address.html.twig @@ -1,23 +1,25 @@ {%- set countPersonLocation = accompanyingCourse.availablePersonLocation|length -%} {%- set hasPersonLocation = countPersonLocation > 0 -%}
        -
        -
        - +
        +
        + +

        + {{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}

        + {% if not hasPersonLocation %} +

        + {{ 'Associate at least one member with an household, and set an address to this household'|trans }}

        + {% endif %}
        -

        - {{ 'This course is located at a temporarily address. You should locate this course to an user'|trans }}

        - {% if not hasPersonLocation %} -

        - {{ 'Associate at least one member with an household, and set an address to this household'|trans }}

        - {% endif %} -
        +
        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 ee578203a..8b0134f6b 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/members_editor.html.twig @@ -4,12 +4,14 @@ {% block title 'household.Edit household members'|trans %} {% block content %} -
        +
        +
        -

        {{ block('title') }}

        -
        +

        {{ block('title') }}

        +
        -
        +
        +
        {% endblock %} {% block js %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/Household/summary.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/Household/summary.html.twig index bbb8ff52f..a00c8df36 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/Household/summary.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/Household/summary.html.twig @@ -51,7 +51,42 @@
        {% if form is null %} - + {% set currentComposition = household.currentComposition %} + {% if currentComposition is not null %} +
        +
        + {{ currentComposition.householdCompositionType.label|localize_translatable_string }} +
        +

        + {{ 'household_composition.numberOfChildren children in household'|trans({'numberOfChildren': currentComposition.numberOfChildren}) }} +

        +

        + {{ 'household_composition.Since'|trans({'startDate': currentComposition.startDate}) }} +

        + +
        + {% else %} +
        +

        + {{ 'household_composition.Currently no composition'|trans }} +

        + +
        + {% endif %} {% if household.waitingForBirth or not household.commentMembers.isEmpty() %}
        {% if household.waitingForBirth %} @@ -176,13 +211,26 @@ {{ 'household.Hide memberships'|trans }} + + {% macro buttonsOldMembers(member) %} + {% set household = member.person.getCurrentHousehold %} + {% if household is not null %} +
      • + +
      • + {% endif %} + {% endmacro %} +
        {% for m in old_members %} - {% include '@ChillPerson/Household/_render_member.html.twig' with { 'member': m } %} + {% include '@ChillPerson/Household/_render_member.html.twig' with { + 'member': m, + 'customButtons': { 'before': _self.buttonsOldMembers(m) } + } %} {% endfor %}
        diff --git a/src/Bundle/ChillPersonBundle/Resources/views/HouseholdComposition/index.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/HouseholdComposition/index.html.twig new file mode 100644 index 000000000..594863a64 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Resources/views/HouseholdComposition/index.html.twig @@ -0,0 +1,96 @@ +{% extends '@ChillPerson/Household/layout.html.twig' %} + +{% block title 'household_composition.Compositions'|trans %} + +{% block block_post_menu %} +
        +{% endblock %} + +{% block content %} +
        +

        {{ block('title') }}

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

        {{ 'household_composition.No composition yet'|trans }}

        + {% else %} +
        + {% for c in compositions %} + {% if c.id != editId %} +
        +
        +
        +

        {{ c.householdCompositionType.label|localize_translatable_string }}

        +

        {{ 'household_composition.numberOfChildren'|trans }}: {{ c.numberOfChildren }}

        +
        +
        {{ 'household_composition.Since'|trans({'startDate': c.startDate}) }}
        +
        +
        +
        + {% if c.endDate is null %} + {{ 'household_composition.Still active'|trans }} + {% else %} + {{ 'household_composition.Until'|trans({'endDate': c.endDate })}} + {% endif %} +
        +
        + {% if c.comment.comment is not empty %} +
        + {{ c.comment|chill_entity_render_box }} +
        + {% endif %} + {% if is_granted('CHILL_PERSON_HOUSEHOLD_EDIT', c.household) %} +
        +
          +
        • + +
        • +
        • + +
        • +
        +
        + {% endif %} +
        + {% else %} + {{ form_start(form) }} + + {{ form_widget(form) }} + + + {{ form_end(form) }} + {% endif %} + {% endfor %} +
        + {% endif %} + +
        + {{ form_start(form) }} + + {{ form_widget(form) }} + +
          +
        • + +
        • +
        + {{ form_end(form) }} +
        + + {% if editId == -1 %} +
          +
        • + +
        • +
        + {% endif %} +
        +{% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Security/Authorization/HouseholdVoter.php b/src/Bundle/ChillPersonBundle/Security/Authorization/HouseholdVoter.php index b702e798e..cdc62c2cf 100644 --- a/src/Bundle/ChillPersonBundle/Security/Authorization/HouseholdVoter.php +++ b/src/Bundle/ChillPersonBundle/Security/Authorization/HouseholdVoter.php @@ -11,7 +11,66 @@ declare(strict_types=1); namespace Chill\PersonBundle\Security\Authorization; -class HouseholdVoter +use Chill\PersonBundle\Entity\Household\Household; +use Chill\PersonBundle\Entity\Household\HouseholdMember; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Authorization\Voter\Voter; +use Symfony\Component\Security\Core\Security; +use UnexpectedValueException; +use function in_array; + +class HouseholdVoter extends Voter { - public const SHOW = PersonVoter::SEE; + public const EDIT = 'CHILL_PERSON_HOUSEHOLD_EDIT'; + + public const SEE = 'CHILL_PERSON_HOUSEHOLD_SEE'; + + /** + * @deprecated use @see{self::SEE} instead + */ + public const SHOW = self::SEE; + + private const ALL = [ + self::EDIT, self::SEE, + ]; + + private Security $security; + + public function __construct(Security $security) + { + $this->security = $security; + } + + protected function supports($attribute, $subject) + { + return $subject instanceof Household + && in_array($attribute, self::ALL, true); + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + { + switch ($attribute) { + case self::SEE: + return $this->checkAssociatedMembersRole($subject, PersonVoter::SEE); + + case self::EDIT: + return $this->checkAssociatedMembersRole($subject, PersonVoter::UPDATE); + + default: + throw new UnexpectedValueException('attribute not supported'); + } + } + + private function checkAssociatedMembersRole(Household $household, string $attribute): bool + { + foreach ($household->getCurrentMembers()->map(static function (HouseholdMember $member) { + return $member->getPerson(); + }) as $person) { + if ($this->security->isGranted($attribute, $person)) { + return true; + } + } + + return false; + } } diff --git a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php index e2fb0f7dc..757a0bfcb 100644 --- a/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php +++ b/src/Bundle/ChillPersonBundle/Serializer/Normalizer/MembersEditorNormalizer.php @@ -12,6 +12,8 @@ declare(strict_types=1); namespace Chill\PersonBundle\Serializer\Normalizer; use Chill\PersonBundle\Entity\Household\Household; +use Chill\PersonBundle\Entity\Household\HouseholdComposition; +use Chill\PersonBundle\Entity\Household\HouseholdCompositionType; use Chill\PersonBundle\Entity\Household\Position; use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Household\MembersEditor; @@ -22,6 +24,7 @@ use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; +use UnexpectedValueException; use function array_key_exists; class MembersEditorNormalizer implements DenormalizerAwareInterface, DenormalizerInterface @@ -148,6 +151,23 @@ class MembersEditorNormalizer implements DenormalizerAwareInterface, Denormalize ); } + if (null !== $data['composition']) { + $compositionType = $this->denormalizer->denormalize($data['composition']['household_composition_type'], HouseholdCompositionType::class, $format, $context); + $numberOfChildren = $data['composition']['number_of_children']; + $startDate = $this->denormalizer->denormalize($data['composition']['start_date'], DateTimeImmutable::class, $format, $context); + + if (null === $compositionType) { + throw new UnexpectedValueException('composition type cannot be null'); + } + + $householdComposition = (new HouseholdComposition()) + ->setHouseholdCompositionType($compositionType) + ->setNumberOfChildren($numberOfChildren) + ->setStartDate($startDate); + + $household->addComposition($householdComposition); + } + return $editor; } diff --git a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php index f2427bfe8..4706a14d8 100644 --- a/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php +++ b/src/Bundle/ChillPersonBundle/Tests/Controller/HouseholdMemberControllerTest.php @@ -169,6 +169,7 @@ final class HouseholdMemberControllerTest extends WebTestCase ], ], 'destination' => null, + 'composition' => null, ] ) ); @@ -223,6 +224,7 @@ final class HouseholdMemberControllerTest extends WebTestCase 'type' => 'household', 'id' => $householdId, ], + 'composition' => null, ] ) ); @@ -272,6 +274,7 @@ final class HouseholdMemberControllerTest extends WebTestCase 'destination' => [ 'type' => 'household', ], + 'composition' => null, ] ) ); diff --git a/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdTest.php b/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdTest.php new file mode 100644 index 000000000..f63c12a2c --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Tests/Entity/Household/HouseholdTest.php @@ -0,0 +1,47 @@ +addComposition(($first = new HouseholdComposition()) + ->setStartDate(new DateTimeImmutable('2021-12-01'))); + + $this->assertNull($first->getEndDate()); + + $household->addComposition(($second = new HouseholdComposition()) + ->setStartDate(new DateTimeImmutable('2021-12-31'))); + + $this->assertEquals(new DateTimeImmutable('2021-12-31'), $first->getEndDate()); + $this->assertEquals(new DateTimeImmutable('2021-12-31'), $second->getStartDate()); + + $household->addComposition(($inside = new HouseholdComposition()) + ->setStartDate(new DateTimeImmutable('2021-12-15'))); + + $this->assertEquals(new DateTimeImmutable('2021-12-15'), $first->getEndDate()); + $this->assertEquals(new DateTimeImmutable('2021-12-31'), $second->getStartDate()); + $this->assertEquals(new DateTimeImmutable('2021-12-31'), $inside->getEndDate()); + } +} diff --git a/src/Bundle/ChillPersonBundle/config/services/security.yaml b/src/Bundle/ChillPersonBundle/config/services/security.yaml index f54d4cdfb..0094e180b 100644 --- a/src/Bundle/ChillPersonBundle/config/services/security.yaml +++ b/src/Bundle/ChillPersonBundle/config/services/security.yaml @@ -25,3 +25,7 @@ services: autoconfigure: true tags: - { name: security.voter } + + Chill\PersonBundle\Security\Authorization\HouseholdVoter: + autowire: true + autoconfigure: true diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20220121121310.php b/src/Bundle/ChillPersonBundle/migrations/Version20220121121310.php new file mode 100644 index 000000000..01fe951e1 --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20220121121310.php @@ -0,0 +1,51 @@ +addSql('DROP SEQUENCE chill_person_household_composition_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_person_household_composition_type_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_person_household_composition'); + $this->addSql('DROP TABLE chill_person_household_composition_type'); + } + + public function getDescription(): string + { + return 'Create table for household composition'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_person_household_composition_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_person_household_composition_type_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_person_household_composition (id INT NOT NULL, household_id INT NOT NULL, endDate DATE DEFAULT NULL, numberOfChildren INT DEFAULT NULL, startDate DATE NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, comment_comment TEXT DEFAULT NULL, comment_date TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, comment_userId INT DEFAULT NULL, householdCompositionType_id INT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_E62BEE83E79FF843 ON chill_person_household_composition (household_id)'); + $this->addSql('CREATE INDEX IDX_E62BEE83CDEA4FCF ON chill_person_household_composition (householdCompositionType_id)'); + $this->addSql('CREATE INDEX IDX_E62BEE833174800F ON chill_person_household_composition (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_E62BEE8365FF1AEC ON chill_person_household_composition (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_person_household_composition.endDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_household_composition.startDate IS \'(DC2Type:date_immutable)\''); + $this->addSql('CREATE TABLE chill_person_household_composition_type (id INT NOT NULL, active BOOLEAN NOT NULL, label JSON NOT NULL, PRIMARY KEY(id))'); + $this->addSql('ALTER TABLE chill_person_household_composition ADD CONSTRAINT FK_E62BEE83E79FF843 FOREIGN KEY (household_id) REFERENCES chill_person_household (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_household_composition ADD CONSTRAINT FK_E62BEE83CDEA4FCF FOREIGN KEY (householdCompositionType_id) REFERENCES chill_person_household_composition_type (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_household_composition ADD CONSTRAINT FK_E62BEE833174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_person_household_composition ADD CONSTRAINT FK_E62BEE8365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('COMMENT ON COLUMN chill_person_household_composition.createdAt IS \'(DC2Type:date_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_person_household_composition.updatedAt IS \'(DC2Type:date_immutable)\''); + } +} diff --git a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml index fcf70c350..ecafb367e 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml +++ b/src/Bundle/ChillPersonBundle/translations/messages+intl-icu.fr.yaml @@ -98,3 +98,17 @@ household: from: Depuis to: Jusqu'au person history: Ménages + +household_composition: + Since: >- + Depuis le {startDate, date, long} + Still active: Toujours actif + Until: >- + Jusqu'au {endDate, date, long} + numberOfChildren children in household: >- + {numberOfChildren, plural, + =0 {Aucun enfant dans le ménage} + one {1 enfant dans le ménage} + few {# enfants dans le ménage} + other {# enfants dans le ménage} + } diff --git a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml index 5d0e2b16a..20de73705 100644 --- a/src/Bundle/ChillPersonBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillPersonBundle/translations/messages.fr.yml @@ -498,3 +498,14 @@ period_notification: You are getting a notification for a period which does not exists any more: Cette notification ne correspond pas à une période d'accompagnement valide. You are getting a notification for a period you are not allowed to see: La notification fait référence à une période d'accompagnement à laquelle vous n'avez pas accès. This is the minimal period details: Période d'accompagnement n° + +household_composition: + No composition yet: Aucune composition familiale renseignée + Compositions: Composition familiale + endDate: Date de fin + numberOfChildren: Nombre d'enfants mineurs au sein du ménage + Household composition: Composition du ménage + Composition added: Information sur la composition familiale ajoutée + Currently no composition: Aucune composition famiale renseignée. + Add a composition: Ajouter une composition familiale + Update composition: Modifier la composition familiale From c7dbaae8d61feff50f1523d6503ecb899fb40553 Mon Sep 17 00:00:00 2001 From: Mathieu Jaumotte Date: Mon, 24 Jan 2022 13:17:46 +0000 Subject: [PATCH 20/21] 105 worflow --- CHANGELOG.md | 7 + .../views/Activity/_list_item.html.twig | 14 +- .../Resources/views/Activity/list.html.twig | 4 - .../Resources/views/Activity/show.html.twig | 2 +- .../_workflow.html.twig | 52 +++ .../_workflow.title.html.twig | 19 + .../AccompanyingCourseDocument/show.html.twig | 100 ++-- ...ompanyingCourseDocumentWorkflowHandler.php | 74 +++ .../ChillDocStoreBundle/config/services.yaml | 5 + .../translations/messages.fr.yml | 1 + .../ChillMainBundle/ChillMainBundle.php | 3 + .../Controller/WorkflowApiController.php | 118 +++++ .../Controller/WorkflowController.php | 257 ++++++++++ .../Entity/Workflow/EntityWorkflow.php | 439 ++++++++++++++++++ .../Entity/Workflow/EntityWorkflowComment.php | 77 +++ .../Entity/Workflow/EntityWorkflowStep.php | 336 ++++++++++++++ .../Form/EntityWorkflowCommentType.php | 27 ++ .../DataTransformer/UserToJsonTransformer.php | 10 +- .../ChillMainBundle/Form/WorkflowStepType.php | 111 +++++ .../Form/WorkflowTransitionType.php | 36 ++ .../Workflow/EntityWorkflowRepository.php | 137 ++++++ .../Resources/public/chill/chillmain.scss | 68 ++- .../Resources/public/chill/scss/buttons.scss | 3 + .../public/chill/scss/flex_table.scss | 9 + .../public/chill/scss/notification.scss | 12 + .../public/chill/scss/render_box.scss | 3 + .../public/lib/entity-workflow/api.js | 12 + .../public/module/bootstrap/index.js | 14 +- .../module/entity-workflow-pick/index.js | 49 ++ .../module/entity-workflow-subscribe/index.js | 32 ++ .../public/module/pick-entity/index.js | 2 +- .../public/module/wopi-link/index.js | 29 ++ .../public/page/workflow-show/index.js | 30 ++ .../EntityWorkflowVueSubscriber.vue | 101 ++++ .../EntityWorkflow/ListWorkflow.vue | 36 ++ .../EntityWorkflow/PickWorkflow.vue | 50 ++ .../public/vuejs/_components/Modal.vue | 13 +- .../public/vuejs/_components/OpenWopiLink.vue | 243 ++++++++++ .../Resources/views/Admin/layout.html.twig | 2 +- .../views/Entity/CommentEmbeddable.html.twig | 24 +- .../views/Menu/verticalMenu.html.twig | 14 +- .../views/Notification/_list_item.html.twig | 8 +- ...ension_counter_notifications_for.html.twig | 14 +- .../views/Notification/list.html.twig | 2 +- .../views/Notification/show.html.twig | 4 +- .../views/Workflow/_attachment.html.twig | 120 +++++ .../views/Workflow/_comment.html.twig | 13 + .../views/Workflow/_decision.html.twig | 48 ++ .../_extension_list_workflow_for.html.twig | 13 + .../views/Workflow/_follow.html.twig | 8 + .../views/Workflow/_history.html.twig | 59 +++ .../Workflow/_notification_include.html.twig | 12 + .../Resources/views/Workflow/index.html.twig | 56 +++ .../Resources/views/Workflow/list.html.twig | 97 ++++ .../views/Workflow/macro_breadcrumb.html.twig | 45 ++ ...n_transition_completed_content.fr.txt.twig | 13 + ..._on_transition_completed_title.fr.txt.twig | 5 + .../Routing/MenuBuilder/UserMenuBuilder.php | 9 + .../Authorization/EntityWorkflowVoter.php | 70 +++ .../Entity/Workflow/EntityWorkflowTest.php | 81 ++++ .../EntityWorkflowHandlerInterface.php | 39 ++ .../Workflow/EntityWorkflowManager.php | 48 ++ ...ntityWorkflowTransitionEventSubscriber.php | 114 +++++ .../NotificationOnTransition.php | 103 ++++ .../Exception/HandlerNotFoundException.php | 18 + .../Workflow/Helper/MetadataExtractor.php | 74 +++ .../WorkflowNotificationHandler.php | 45 ++ .../RelatedEntityWorkflowSupportsStrategy.php | 35 ++ .../Templating/WorkflowTwigExtension.php | 29 ++ .../WorkflowTwigExtensionRuntime.php | 79 ++++ .../Validator/EntityWorkflowCreation.php | 35 ++ .../EntityWorkflowCreationValidator.php | 71 +++ .../ChillMainBundle/chill.webpack.config.js | 4 + .../ChillMainBundle/config/services.yaml | 14 + .../ChillMainBundle/config/services/form.yaml | 2 + .../config/services/security.yaml | 2 + .../migrations/Version20220112123436.php | 72 +++ .../migrations/Version20220114132105.php | 47 ++ .../migrations/Version20220114165950.php | 37 ++ .../translations/messages.fr.yml | 40 +- ...mpanyingPeriodWorkEvaluationRepository.php | 61 +++ .../vuejs/AccompanyingCourseWorkEdit/App.vue | 20 +- .../components/AddEvaluation.vue | 35 +- .../components/FormEvaluation.vue | 36 +- .../vuejs/AccompanyingCourseWorkEdit/store.js | 4 +- .../Comment/index.html.twig | 6 +- .../views/AccompanyingCourse/index.html.twig | 19 +- .../AccompanyingCourseWork/_item.html.twig | 127 +++++ .../AccompanyingCourseWork/edit.html.twig | 23 +- .../AccompanyingCourseWork/index.html.twig | 132 +----- ...st_recent_by_accompanying_period.html.twig | 9 +- .../views/AccompanyingPeriod/_list.html.twig | 4 - .../AccompanyingPeriod/_list_item.html.twig | 20 +- .../Resources/views/Macro/updatedBy.html.twig | 12 + .../views/Person/household_history.html.twig | 2 +- .../views/Person/list_with_period.html.twig | 247 +++++----- .../Resources/views/Person/view.html.twig | 6 +- .../_accompanying_period_work.html.twig | 17 + .../_accompanying_period_work.title.html.twig | 19 + .../views/Workflow/_evaluation.html.twig | 99 ++++ .../Workflow/_evaluation.title.html.twig | 19 + .../AccompanyingPeriodWorkEvaluationVoter.php | 51 ++ .../AccompanyingPeriodWorkVoter.php | 57 +++ ...mpanyingPeriodWorkEvaluationNormalizer.php | 74 +++ .../AccompanyingPeriodWorkNormalizer.php | 85 ++++ .../Normalizer/WorkflowNormalizer.php | 70 +++ ...ingPeriodWorkEvaluationWorkflowHandler.php | 71 +++ .../AccompanyingPeriodWorkWorkflowHandler.php | 70 +++ .../ChillPersonBundle/config/services.yaml | 5 + .../config/services/security.yaml | 8 + 110 files changed, 5176 insertions(+), 392 deletions(-) create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig create mode 100644 src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.title.html.twig create mode 100644 src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php create mode 100644 src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php create mode 100644 src/Bundle/ChillMainBundle/Controller/WorkflowController.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php create mode 100644 src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php create mode 100644 src/Bundle/ChillMainBundle/Form/EntityWorkflowCommentType.php create mode 100644 src/Bundle/ChillMainBundle/Form/WorkflowStepType.php create mode 100644 src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php create mode 100644 src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php create mode 100644 src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-pick/index.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-subscribe/index.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflow.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/OpenWopiLink.vue create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_comment.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_extension_list_workflow_for.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_follow.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/_notification_include.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/list.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig create mode 100644 src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig create mode 100644 src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php create mode 100644 src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EntityWorkflowManager.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Exception/HandlerNotFoundException.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Helper/MetadataExtractor.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Notification/WorkflowNotificationHandler.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/RelatedEntityWorkflowSupportsStrategy.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtension.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtensionRuntime.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Validator/EntityWorkflowCreation.php create mode 100644 src/Bundle/ChillMainBundle/Workflow/Validator/EntityWorkflowCreationValidator.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220112123436.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220114132105.php create mode 100644 src/Bundle/ChillMainBundle/migrations/Version20220114165950.php create mode 100644 src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/AccompanyingCourseWork/_item.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Macro/updatedBy.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Workflow/_accompanying_period_work.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Workflow/_accompanying_period_work.title.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation.title.html.twig create mode 100644 src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkEvaluationVoter.php create mode 100644 src/Bundle/ChillPersonBundle/Security/Authorization/AccompanyingPeriodWorkVoter.php create mode 100644 src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodWorkEvaluationNormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/Serializer/Normalizer/AccompanyingPeriodWorkNormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/Serializer/Normalizer/WorkflowNormalizer.php create mode 100644 src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkEvaluationWorkflowHandler.php create mode 100644 src/Bundle/ChillPersonBundle/Workflow/AccompanyingPeriodWorkWorkflowHandler.php diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3a8da23..9fe5fc94b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,9 @@ and this project adheres to * [notification] add `[Chill]` in the subject of each notification, automatically * [notification] add a counter for notification in activity list and accompanying period list, and search results * [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before. +* [workflow] add breadcrumb to show steps +* [popover] add popover html popup mechanism (used by workflow breadcrumb) +* [templates] improve updatedBy macro in item metadatas ## Test releases @@ -28,6 +31,10 @@ and this project adheres to * [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246) * [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176) * [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380) +* [workflow] + * add My workflow section with my opened subscriptions + * apply workflow on documents, accompanyingCourseWork and Evaluations +* [wopi-link] a new vue component allow to open wopi link in a fullscreen chill-themed modal ### test release 2022-01-17 diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig index 09a7ab2a3..6b2e33dfb 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/_list_item.html.twig @@ -143,9 +143,17 @@
        -
          - {{ recordAction }} -
        +
        + {% set notif_counter = chill_count_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) %} + {% if notif_counter.total > 0 %} + {{ chill_counter_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) }} + {% endif %} +
        +
        +
          + {{ recordAction }} +
        +
        diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig index 6b0639a27..7ae24fa4b 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/list.html.twig @@ -1,10 +1,6 @@ {% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %} {% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %} {% if no_action is not defined or no_action == false %} - {% set notif_counter = chill_count_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) %} - {% if notif_counter.total > 0 %} -
      • {{ chill_counter_notifications('Chill\\ActivityBundle\\Entity\\Activity', activity.id) }}
      • - {% endif %}
      • {{ 'Activity data'|trans }} diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig new file mode 100644 index 000000000..4ec11a8e8 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig @@ -0,0 +1,52 @@ +{% import "@ChillDocStore/Macro/macro.html.twig" as m %} + +
        +
        +
        +
        + +
        +
        +

        {{ document.title }}

        + {{ document.object.type }} + + {% if document.description is not empty %} +
        + {{ document.description }} +
        + {% endif %} + +
        +
        +
        +
        + +{% if display_action is defined and display_action == true %} +
          +
        • + {{ m.download_button(document.object, document.title) }} +
        • +
        • + + {# + data-button is optional ! + OPTIONS: + 'changeIcon' string + 'changeClass' string + 'noText' boolean + + #}{% set button = { + 'changeIcon': 'fa-unlock', + } %} + + {# vue component #} + +
        • +
        +{% endif %} \ No newline at end of file diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.title.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.title.html.twig new file mode 100644 index 000000000..e3c1dc50a --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.title.html.twig @@ -0,0 +1,19 @@ +{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as m %} + +
        +
        + {% if concerne is defined and concerne == true %} + {{ 'Concerne'|trans }}: + {% endif %} + + {{ 'workflow.Document (n°%doc%)'|trans({'%doc%': document.id}) }} + + {% if description is defined and description == true %} + {{ ' — ' ~ document.title }} + {% endif %} +
        + + {% if breadcrumb is defined and breadcrumb == true %} + {{ m.breadcrumb(_context) }} + {% endif %} +
        \ No newline at end of file diff --git a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig index 97299675a..9d6175e12 100644 --- a/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig +++ b/src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/show.html.twig @@ -6,61 +6,71 @@ {% block title %} {# {{ 'Detail of document of %name%'|trans({ '%name%': accompanyingCourse|chill_entity_render_string } ) }} #} -{% endblock %} - - -{% block js %} - {{ parent() }} - {{ encore_entry_script_tags('mod_async_upload') }} + {{ 'Document %title%' | trans({ '%title%': document.title }) }} {% endblock %} {% block css %} {{ parent() }} {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_link_tags('mod_entity_workflow_pick') }} {% endblock %} {% block content %} - -

        {{ 'Document %title%' | trans({ '%title%': document.title }) }}

        - -
        -
        {{ 'Title'|trans }}
        -
        {{ document.title }}
        - +
        +{% endblock %} + +{% block block_post_menu %} +
        + {% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %} + {% if workflows_frame is not empty %} + {{ workflows_frame|raw }} + {% endif %} +
        +{% endblock %} -
        {{ 'Description' | trans }}
        -
        - {% if document.description is empty %} - {{ 'Any description'|trans }} - {% else %} -
        - {{ document.description|chill_markdown_to_html }} -
        - {% endif %} -
        - -
        - -
          -
        • - - {{ 'Back to the list' | trans }} - -
        • - -
        • - {{ m.download_button(document.object, document.title) }} -
        • - - {% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %} -
        • - - {{ 'Edit' | trans }} - -
        • - {% endif %} - {% endblock %} +{% block js %} + {{ parent() }} + {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_entity_workflow_pick') }} +{% endblock %} \ No newline at end of file diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php new file mode 100644 index 000000000..a615ddda2 --- /dev/null +++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php @@ -0,0 +1,74 @@ +repository = $em->getRepository(AccompanyingCourseDocument::class); + } + + public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?AccompanyingCourseDocument + { + return $this->repository->find($entityWorkflow->getRelatedEntityId()); + } + + public function getRoleShow(EntityWorkflow $entityWorkflow): ?string + { + return null; + } + + public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string + { + return '@ChillDocStore/AccompanyingCourseDocument/_workflow.html.twig'; + } + + public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array + { + return [ + 'entity_workflow' => $entityWorkflow, + 'document' => $this->getRelatedEntity($entityWorkflow), + ]; + } + + public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string + { + return '@ChillDocStore/AccompanyingCourseDocument/_workflow.title.html.twig'; + } + + public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array + { + return $this->getTemplateData($entityWorkflow, $options); + } + + public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool + { + return $entityWorkflow->getRelatedEntityClass() === AccompanyingCourseDocument::class; + } + + public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool + { + return true; + } +} diff --git a/src/Bundle/ChillDocStoreBundle/config/services.yaml b/src/Bundle/ChillDocStoreBundle/config/services.yaml index 2766cb1c7..6685d22eb 100644 --- a/src/Bundle/ChillDocStoreBundle/config/services.yaml +++ b/src/Bundle/ChillDocStoreBundle/config/services.yaml @@ -27,3 +27,8 @@ services: autoconfigure: true tags: - { name: chill.role } + + Chill\DocStoreBundle\Workflow\: + resource: './../Workflow/' + autoconfigure: true + autowire: true diff --git a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml index 947471531..3ee10721c 100644 --- a/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillDocStoreBundle/translations/messages.fr.yml @@ -9,6 +9,7 @@ Create new document: Créer un nouveau document New document for %name%: Nouveau document pour %name% Editing document for %name%: Modification d'un document pour %name% Edit Document: Modification d'un document +Update document: Modifier le document Existing document: Document existant No document to download: Aucun document à télécharger 'Choose a document category': Choisissez une catégorie de document diff --git a/src/Bundle/ChillMainBundle/ChillMainBundle.php b/src/Bundle/ChillMainBundle/ChillMainBundle.php index 7a4e563bb..07daf65ea 100644 --- a/src/Bundle/ChillMainBundle/ChillMainBundle.php +++ b/src/Bundle/ChillMainBundle/ChillMainBundle.php @@ -31,6 +31,7 @@ use Chill\MainBundle\Security\Resolver\ScopeResolverInterface; use Chill\MainBundle\Templating\Entity\ChillEntityRenderInterface; use Chill\MainBundle\Templating\Entity\CompilerPass as RenderEntityCompilerPass; use Chill\MainBundle\Templating\UI\NotificationCounterInterface; +use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -56,6 +57,8 @@ class ChillMainBundle extends Bundle ->addTag('chill_main.notification_handler'); $container->registerForAutoconfiguration(NotificationCounterInterface::class) ->addTag('chill.count_notification.user'); + $container->registerForAutoconfiguration(EntityWorkflowHandlerInterface::class) + ->addTag('chill_main.workflow_handler'); $container->addCompilerPass(new SearchableServicesCompilerPass()); $container->addCompilerPass(new ConfigConsistencyCompilerPass()); diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php new file mode 100644 index 000000000..dda787708 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php @@ -0,0 +1,118 @@ +entityManager = $entityManager; + $this->security = $security; + } + + /** + * @Route("/api/1.0/main/workflow/{id}/subscribe", methods={"POST"}) + */ + public function subscribe(EntityWorkflow $entityWorkflow, Request $request): Response + { + return $this->handleSubscription($entityWorkflow, $request, 'subscribe'); + } + + /** + * @Route("/api/1.0/main/workflow/{id}/unsubscribe", methods={"POST"}) + */ + public function unsubscribe(EntityWorkflow $entityWorkflow, Request $request): Response + { + return $this->handleSubscription($entityWorkflow, $request, 'unsubscribe'); + } + + private function handleSubscription(EntityWorkflow $entityWorkflow, Request $request, string $action): JsonResponse + { + if (!$this->security->isGranted('IS_AUTHENTICATED_REMEMBERED')) { + throw new AccessDeniedException(); + } + + if (!$request->query->has('subscribe')) { + throw new BadRequestHttpException('missing subscribe parameter'); + } + + $user = $this->security->getUser(); + + switch ($request->query->get('subscribe')) { + case 'final': + switch ($action) { + case 'subscribe': + $entityWorkflow->addSubscriberToFinal($user); + + break; + + case 'unsubscribe': + $entityWorkflow->removeSubscriberToFinal($user); + + break; + + default: + throw new LogicException(); + } + + break; + + case 'step': + switch ($action) { + case 'subscribe': + $entityWorkflow->addSubscriberToStep($user); + + break; + + case 'unsubscribe': + $entityWorkflow->removeSubscriberToStep($user); + + break; + + default: + throw new LogicException(); + } + + break; + + default: + throw new BadRequestHttpException('subscribe parameter must be equal to "step" or "final"'); + } + + $this->entityManager->flush(); + + return new JsonResponse( + [ + 'step' => $entityWorkflow->isUserSubscribedToStep($user), + 'final' => $entityWorkflow->isUserSubscribedToFinal($user), + ], + JsonResponse::HTTP_OK, + [], + false + ); + } +} diff --git a/src/Bundle/ChillMainBundle/Controller/WorkflowController.php b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php new file mode 100644 index 000000000..33f1bd774 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Controller/WorkflowController.php @@ -0,0 +1,257 @@ +entityWorkflowManager = $entityWorkflowManager; + $this->entityWorkflowRepository = $entityWorkflowRepository; + $this->validator = $validator; + $this->paginatorFactory = $paginatorFactory; + $this->registry = $registry; + $this->entityManager = $entityManager; + $this->translator = $translator; + } + + /** + * @Route("/{_locale}/main/workflow/create", name="chill_main_workflow_create") + */ + public function create(Request $request): Response + { + if (!$request->query->has('entityClass')) { + throw new BadRequestHttpException('Missing entityClass parameter'); + } + + if (!$request->query->has('entityId')) { + throw new BadRequestHttpException('missing entityId parameter'); + } + + if (!$request->query->has('workflow')) { + throw new BadRequestHttpException('missing workflow parameter'); + } + + $entityWorkflow = new EntityWorkflow(); + $entityWorkflow + ->setRelatedEntityClass($request->query->get('entityClass')) + ->setRelatedEntityId($request->query->getInt('entityId')) + ->setWorkflowName($request->query->get('workflow')); + + $errors = $this->validator->validate($entityWorkflow, null, ['creation']); + + if (count($errors) > 0) { + $msg = []; + + foreach ($errors as $error) { + /** @var \Symfony\Component\Validator\ConstraintViolationInterface $error */ + $msg[] = $error->getMessage(); + } + + return new Response(implode("\n", $msg), Response::HTTP_UNPROCESSABLE_ENTITY); + } + + $this->denyAccessUnlessGranted(EntityWorkflowVoter::CREATE, $entityWorkflow); + + $em = $this->getDoctrine()->getManager(); + $em->persist($entityWorkflow); + $em->flush(); + + return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]); + } + + /** + * @Route("/{_locale}/main/workflow/list/dest", name="chill_main_workflow_list_dest") + */ + public function myWorkflowsDest(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $total = $this->entityWorkflowRepository->countByDest($this->getUser()); + $paginator = $this->paginatorFactory->create($total); + + $workflows = $this->entityWorkflowRepository->findByDest( + $this->getUser(), + ['createdAt' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + return $this->render( + '@ChillMain/Workflow/list.html.twig', + [ + 'workflows' => $this->buildHandler($workflows), + 'paginator' => $paginator, + 'step' => 'dest', + ] + ); + } + + /** + * @Route("/{_locale}/main/workflow/list/subscribed", name="chill_main_workflow_list_subscribed") + */ + public function myWorkflowsSubscribed(Request $request): Response + { + $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED'); + + $total = $this->entityWorkflowRepository->countBySubscriber($this->getUser()); + $paginator = $this->paginatorFactory->create($total); + + $workflows = $this->entityWorkflowRepository->findBySubscriber( + $this->getUser(), + ['createdAt' => 'DESC'], + $paginator->getItemsPerPage(), + $paginator->getCurrentPageFirstItemNumber() + ); + + return $this->render( + '@ChillMain/Workflow/list.html.twig', + [ + 'workflows' => $this->buildHandler($workflows), + 'paginator' => $paginator, + 'step' => 'subscribed', + ] + ); + } + + /** + * @Route("/{_locale}/main/workflow/{id}/show", name="chill_main_workflow_show") + */ + public function show(EntityWorkflow $entityWorkflow, Request $request): Response + { + $this->denyAccessUnlessGranted(EntityWorkflowVoter::SEE, $entityWorkflow); + + $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + + if (count($workflow->getEnabledTransitions($entityWorkflow)) > 0) { + // possible transition + $transitionForm = $this->createForm( + WorkflowStepType::class, + $entityWorkflow->getCurrentStep(), + ['transition' => true, 'entity_workflow' => $entityWorkflow] + ); + + $transitionForm->handleRequest($request); + + if ($transitionForm->isSubmitted() && $transitionForm->isValid()) { + if (!$workflow->can($entityWorkflow, $transition = $transitionForm['transition']->getData()->getName())) { + $blockers = $workflow->buildTransitionBlockerList($entityWorkflow, $transition); + $msgs = array_map(function (TransitionBlocker $tb) { + return $this->translator->trans( + $tb->getMessage(), + $tb->getParameters() + ); + }, iterator_to_array($blockers)); + + throw $this->createAccessDeniedException( + sprintf( + "not allowed to apply transition {$transition}: %s", + implode(', ', $msgs) + ) + ); + } + + $workflow->apply($entityWorkflow, $transition); + + foreach ($transitionForm['future_dest_users']->getData() as $user) { + $entityWorkflow->getCurrentStep()->addDestUser($user); + } + + $this->entityManager->flush(); + + return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]); + } + + if ($transitionForm->isSubmitted() && !$transitionForm->isValid()) { + $this->addFlash('error', $this->translator->trans('This form contains errors')); + } + } + + /* + $commentForm = $this->createForm(EntityWorkflowCommentType::class, $newComment = new EntityWorkflowComment()); + $commentForm->handleRequest($request); + + if ($commentForm->isSubmitted() && $commentForm->isValid()) { + $this->entityManager->persist($newComment); + $this->entityManager->flush(); + + $this->addFlash('success', $this->translator->trans('workflow.Comment added')); + + return $this->redirectToRoute('chill_main_workflow_show', ['id' => $entityWorkflow->getId()]); + } elseif ($commentForm->isSubmitted() && !$commentForm->isValid()) { + $this->addFlash('error', $this->translator->trans('This form contains errors')); + } + */ + + return $this->render( + '@ChillMain/Workflow/index.html.twig', + [ + 'handler_template' => $handler->getTemplate($entityWorkflow), + 'handler_template_title' => $handler->getTemplateTitle($entityWorkflow), + 'handler_template_data' => $handler->getTemplateData($entityWorkflow), + 'transition_form' => isset($transitionForm) ? $transitionForm->createView() : null, + 'entity_workflow' => $entityWorkflow, + //'comment_form' => $commentForm->createView(), + ] + ); + } + + private function buildHandler(array $workflows): array + { + $lines = []; + + foreach ($workflows as $workflow) { + $handler = $this->entityWorkflowManager->getHandler($workflow); + $lines[] = [ + 'handler' => $handler, + 'entity_workflow' => $workflow, + ]; + } + + return $lines; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php new file mode 100644 index 000000000..b56435f5d --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php @@ -0,0 +1,439 @@ +subscriberToFinal = new ArrayCollection(); + $this->subscriberToStep = new ArrayCollection(); + $this->comments = new ArrayCollection(); + $this->steps = new ArrayCollection(); + + $initialStep = new EntityWorkflowStep(); + $initialStep + ->setCurrentStep('initial'); + $this->addStep($initialStep); + } + + public function addComment(EntityWorkflowComment $comment): self + { + if (!$this->comments->contains($comment)) { + $this->comments[] = $comment; + $comment->setEntityWorkflow($this); + } + + return $this; + } + + /** + * @internal You should prepare a step and run a workflow transition instead of manually adding a step + */ + public function addStep(EntityWorkflowStep $step): self + { + if (!$this->steps->contains($step)) { + $this->steps[] = $step; + $step->setEntityWorkflow($this); + + if ($this->isFinalize()) { + $step->setFinalizeAfter(true); + } + } + + return $this; + } + + public function addSubscriberToFinal(User $user): self + { + if (!$this->subscriberToFinal->contains($user)) { + $this->subscriberToFinal[] = $user; + } + + return $this; + } + + public function addSubscriberToStep(User $user): self + { + if (!$this->subscriberToStep->contains($user)) { + $this->subscriberToStep[] = $user; + } + + return $this; + } + + public function getComments(): Collection + { + return $this->comments; + } + + public function getCurrentStep(): ?EntityWorkflowStep + { + $step = $this->steps->last(); + + if (false !== $step) { + return $step; + } + + return null; + } + + public function getCurrentStepCreatedAt(): ?DateTimeInterface + { + if (null !== $previous = $this->getPreviousStepIfAny()) { + return $previous->getTransitionAt(); + } + + return null; + } + + public function getCurrentStepCreatedBy(): ?User + { + if (null !== $previous = $this->getPreviousStepIfAny()) { + return $previous->getTransitionBy(); + } + + return null; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getRelatedEntityClass(): string + { + return $this->relatedEntityClass; + } + + public function getRelatedEntityId(): int + { + return $this->relatedEntityId; + } + + /** + * Method used by MarkingStore. + * + * get a string representation of the step + */ + public function getStep(): string + { + return $this->getCurrentStep()->getCurrentStep(); + } + + public function getStepAfter(EntityWorkflowStep $step): ?EntityWorkflowStep + { + $iterator = $this->steps->getIterator(); + + if ($iterator instanceof Iterator) { + $iterator->rewind(); + + while ($iterator->valid()) { + $curStep = $iterator->current(); + + if ($curStep === $step) { + $iterator->next(); + + if ($iterator->valid()) { + return $iterator->current(); + } + + return null; + } + $iterator->next(); + } + + return null; + } + + throw new RuntimeException(); + } + + /** + * @return ArrayCollection|Collection + */ + public function getSteps() + { + return $this->steps; + } + + public function getStepsChained(): array + { + $iterator = $this->steps->getIterator(); + $previous = $next = $current = null; + $steps = []; + + $iterator->rewind(); + + while ($iterator->valid()) { + $previous = $current; + $steps[] = $current = $iterator->current(); + $current->setPrevious($previous); + + $iterator->next(); + + if ($iterator->valid()) { + $next = $iterator->current(); + } else { + $next = null; + } + + $current->setNext($next); + } + + return $steps; + } + + /** + * @return ArrayCollection|Collection + */ + public function getSubscriberToFinal() + { + return $this->subscriberToFinal; + } + + /** + * @return ArrayCollection|Collection + */ + public function getSubscriberToStep() + { + return $this->subscriberToStep; + } + + /** + * get the step which is transitionning. Should be called only by event which will + * concern the transition. + */ + public function getTransitionningStep(): ?EntityWorkflowStep + { + return $this->transitionningStep; + } + + public function getWorkflowName(): string + { + return $this->workflowName; + } + + public function isFinalize(): bool + { + $steps = $this->getStepsChained(); + + if (1 === count($steps)) { + // the initial step cannot be finalized + return false; + } + + /** @var EntityWorkflowStep $last */ + $last = end($steps); + + return $last->getPrevious()->isFinalizeAfter(); + } + + public function isFreeze(): bool + { + $steps = $this->getStepsChained(); + + if (1 === count($steps)) { + // the initial step cannot be finalized + return false; + } + + /** @var EntityWorkflowStep $last */ + $last = end($steps); + + return $last->getPrevious()->isFreezeAfter(); + } + + public function isUserSubscribedToFinal(User $user): bool + { + return $this->subscriberToFinal->contains($user); + } + + public function isUserSubscribedToStep(User $user): bool + { + return $this->subscriberToStep->contains($user); + } + + public function prepareStepBeforeTransition(EntityWorkflowStep $step): self + { + $this->transitionningStep = $step; + + return $this; + } + + public function removeComment(EntityWorkflowComment $comment): self + { + if ($this->comments->removeElement($comment)) { + $comment->setEntityWorkflow(null); + } + + return $this; + } + + public function removeStep(EntityWorkflowStep $step): self + { + if ($this->steps->removeElement($step)) { + $step->setEntityWorkflow(null); + } + + return $this; + } + + public function removeSubscriberToFinal(User $user): self + { + $this->subscriberToFinal->removeElement($user); + + return $this; + } + + public function removeSubscriberToStep(User $user): self + { + $this->subscriberToStep->removeElement($user); + + return $this; + } + + public function setRelatedEntityClass(string $relatedEntityClass): EntityWorkflow + { + $this->relatedEntityClass = $relatedEntityClass; + + return $this; + } + + public function setRelatedEntityId(int $relatedEntityId): EntityWorkflow + { + $this->relatedEntityId = $relatedEntityId; + + return $this; + } + + /** + * Method use by marking store. + * + * @return $this + */ + public function setStep(string $step): self + { + $newStep = new EntityWorkflowStep(); + $newStep->setCurrentStep($step); + + // copy the freeze + if ($this->getCurrentStep()->isFreezeAfter()) { + $newStep->setFreezeAfter(true); + } + + $this->addStep($newStep); + + return $this; + } + + public function setWorkflowName(string $workflowName): EntityWorkflow + { + $this->workflowName = $workflowName; + + return $this; + } + + private function getPreviousStepIfAny(): ?EntityWorkflowStep + { + if (1 === count($this->steps)) { + return null; + } + + return $this->steps->get($this->steps->count() - 2); + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php new file mode 100644 index 000000000..b041a5aa3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowComment.php @@ -0,0 +1,77 @@ +comment; + } + + public function getEntityWorkflow(): ?EntityWorkflow + { + return $this->entityWorkflow; + } + + public function getId(): ?int + { + return $this->id; + } + + public function setComment(string $comment): self + { + $this->comment = $comment; + + return $this; + } + + /** + * @internal use @see{EntityWorkflow::addComment} + */ + public function setEntityWorkflow(?EntityWorkflow $entityWorkflow): self + { + $this->entityWorkflow = $entityWorkflow; + + return $this; + } +} diff --git a/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php new file mode 100644 index 000000000..6bd39ae0c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflowStep.php @@ -0,0 +1,336 @@ +destUser = new ArrayCollection(); + } + + public function addDestEmail(string $email): self + { + if (!in_array($email, $this->destEmail, true)) { + $this->destEmail[] = $email; + } + + return $this; + } + + public function addDestUser(User $user): self + { + if (!$this->destUser->contains($user)) { + $this->destUser[] = $user; + } + + return $this; + } + + public function getComment(): string + { + return $this->comment; + } + + public function getCurrentStep(): ?string + { + return $this->currentStep; + } + + public function getDestEmail(): array + { + return $this->destEmail; + } + + /** + * @return ArrayCollection|Collection + */ + public function getDestUser() + { + return $this->destUser; + } + + public function getEntityWorkflow(): ?EntityWorkflow + { + return $this->entityWorkflow; + } + + public function getId(): ?int + { + return $this->id; + } + + public function getNext(): ?EntityWorkflowStep + { + return $this->next; + } + + public function getPrevious(): ?EntityWorkflowStep + { + return $this->previous; + } + + public function getTransitionAfter(): ?string + { + return $this->transitionAfter; + } + + public function getTransitionAt(): ?DateTimeImmutable + { + return $this->transitionAt; + } + + public function getTransitionBy(): ?User + { + return $this->transitionBy; + } + + public function getTransitionByEmail(): ?string + { + return $this->transitionByEmail; + } + + public function isFinalizeAfter(): bool + { + return $this->finalizeAfter; + } + + public function isFreezeAfter(): bool + { + return $this->freezeAfter; + } + + public function removeDestEmail(string $email): self + { + $this->destEmail = array_filter($this->destEmail, static function (string $existing) use ($email) { + return $email !== $existing; + }); + + return $this; + } + + public function removeDestUser(User $user): self + { + $this->destUser->removeElement($user); + + return $this; + } + + public function setComment(?string $comment): EntityWorkflowStep + { + $this->comment = (string) $comment; + + return $this; + } + + public function setCurrentStep(?string $currentStep): EntityWorkflowStep + { + $this->currentStep = $currentStep; + + return $this; + } + + public function setDestEmail(array $destEmail): EntityWorkflowStep + { + $this->destEmail = $destEmail; + + return $this; + } + + /** + * @internal use @see(EntityWorkflow::addStep} instead + */ + public function setEntityWorkflow(?EntityWorkflow $entityWorkflow): EntityWorkflowStep + { + $this->entityWorkflow = $entityWorkflow; + + return $this; + } + + public function setFinalizeAfter(bool $finalizeAfter): EntityWorkflowStep + { + $this->finalizeAfter = $finalizeAfter; + + return $this; + } + + public function setFreezeAfter(bool $freezeAfter): EntityWorkflowStep + { + $this->freezeAfter = $freezeAfter; + + return $this; + } + + /** + * @return EntityWorkflowStep + * + * @internal + */ + public function setNext(?EntityWorkflowStep $next): self + { + $this->next = $next; + + return $this; + } + + /** + * @return EntityWorkflowStep + * + * @internal + */ + public function setPrevious(?EntityWorkflowStep $previous): self + { + $this->previous = $previous; + + return $this; + } + + public function setTransitionAfter(?string $transitionAfter): EntityWorkflowStep + { + $this->transitionAfter = $transitionAfter; + + return $this; + } + + public function setTransitionAt(?DateTimeImmutable $transitionAt): EntityWorkflowStep + { + $this->transitionAt = $transitionAt; + + return $this; + } + + public function setTransitionBy(?User $transitionBy): EntityWorkflowStep + { + $this->transitionBy = $transitionBy; + + return $this; + } + + public function setTransitionByEmail(?string $transitionByEmail): EntityWorkflowStep + { + $this->transitionByEmail = $transitionByEmail; + + return $this; + } + + /** + * @Assert\Callback + * + * @param mixed $payload + */ + public function validateOnCreation(ExecutionContextInterface $context, $payload): void + { + return; + + if ($this->isFinalizeAfter()) { + if (0 !== count($this->getDestUser())) { + $context->buildViolation('workflow.No dest users when the workflow is finalized') + ->atPath('finalizeAfter') + ->addViolation(); + } + } else { + if (0 === count($this->getDestUser())) { + $context->buildViolation('workflow.The next step must count at least one dest') + ->atPath('finalizeAfter') + ->addViolation(); + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/Form/EntityWorkflowCommentType.php b/src/Bundle/ChillMainBundle/Form/EntityWorkflowCommentType.php new file mode 100644 index 000000000..c32e09dfd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/EntityWorkflowCommentType.php @@ -0,0 +1,27 @@ +add('comment', ChillTextareaType::class, [ + 'required' => false, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php index df670f891..ca87ea4f5 100644 --- a/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php +++ b/src/Bundle/ChillMainBundle/Form/Type/DataTransformer/UserToJsonTransformer.php @@ -36,14 +36,20 @@ class UserToJsonTransformer implements DataTransformerInterface public function reverseTransform($value) { + $denormalized = json_decode($value, true); + if ($this->multiple) { + if (null === $denormalized) { + return []; + } + return array_map( function ($item) { return $this->denormalizeOne($item); }, - json_decode($value, true) + $denormalized ); } - return $this->denormalizeOne(json_decode($value, true)); + return $this->denormalizeOne($denormalized); } /** diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php new file mode 100644 index 000000000..6600e330a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/WorkflowStepType.php @@ -0,0 +1,111 @@ +entityWorkflowManager = $entityWorkflowManager; + $this->registry = $registry; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + /** @var \Chill\MainBundle\Entity\Workflow\EntityWorkflow $entityWorkflow */ + $entityWorkflow = $options['entity_workflow']; + $handler = $this->entityWorkflowManager->getHandler($entityWorkflow); + + if (true === $options['transition']) { + if (null === $options['entity_workflow']) { + throw new LogicException('if transition is true, entity_workflow should be defined'); + } + + $transitions = $this->registry + ->get($options['entity_workflow'], $entityWorkflow->getWorkflowName()) + ->getEnabledTransitions($entityWorkflow); + + $choices = array_combine( + array_map(static function (Transition $transition) { return $transition->getName(); }, $transitions), + $transitions + ); + + $builder + ->add('transition', ChoiceType::class, [ + 'label' => 'workflow.Transition', + 'mapped' => false, + 'multiple' => false, + 'expanded' => true, + 'choices' => $choices, + 'choice_label' => static function (Transition $transition) { + return implode(', ', $transition->getTos()); + }, + ]) + ->add('future_dest_users', PickUserDynamicType::class, [ + 'label' => 'workflow.dest for next steps', + 'multiple' => true, + 'mapped' => false, + ]); + } + + if ( + $handler->supportsFreeze($entityWorkflow) + && !$entityWorkflow->isFreeze() + ) { + $builder + ->add('freezeAfter', CheckboxType::class, [ + 'required' => false, + 'label' => 'workflow.Freeze', + 'help' => 'workflow.The associated element will be freezed', + ]); + } + + $builder + ->add('finalizeAfter', CheckboxType::class, [ + 'required' => false, + 'label' => 'workflow.Finalize', + 'help' => 'workflow.The workflow will be finalized', + ]) + ->add('comment', ChillTextareaType::class, [ + 'required' => false, + 'label' => 'Comment', + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefined('class', EntityWorkflowStep::class) + ->setRequired('transition') + ->setAllowedTypes('transition', 'bool') + ->setRequired('entity_workflow') + ->setAllowedTypes('entity_workflow', EntityWorkflow::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php b/src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php new file mode 100644 index 000000000..1692928dd --- /dev/null +++ b/src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php @@ -0,0 +1,36 @@ +add('current_step', WorkflowStepType::class, [ + 'transition' => true, + 'entity_workflow' => $options['entity_workflow'], + ]); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setRequired('entity_workflow') + ->setAllowedTypes('entity_workflow', EntityWorkflow::class); + } +} diff --git a/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php new file mode 100644 index 000000000..cac5c9996 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Repository/Workflow/EntityWorkflowRepository.php @@ -0,0 +1,137 @@ +repository = $entityManager->getRepository(EntityWorkflow::class); + } + + public function countByDest(User $user): int + { + $qb = $this->buildQueryByDest($user)->select('count(ew)'); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + + public function countBySubscriber(User $user): int + { + $qb = $this->buildQueryBySubscriber($user)->select('count(ew)'); + + return (int) $qb->getQuery()->getSingleScalarResult(); + } + + public function find($id): ?EntityWorkflow + { + return $this->repository->find($id); + } + + /** + * @return array|EntityWorkflow[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @param null|mixed $limit + * @param null|mixed $offset + * + * @return array|EntityWorkflow[] + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findByDest(User $user, ?array $orderBy = null, $limit = null, $offset = null): array + { + $qb = $this->buildQueryByDest($user)->select('ew'); + + foreach ($orderBy as $key => $sort) { + $qb->addOrderBy('ew.' . $key, $sort); + } + + $qb->setMaxResults($limit)->setFirstResult($offset); + + return $qb->getQuery()->getResult(); + } + + public function findBySubscriber(User $user, ?array $orderBy = null, $limit = null, $offset = null): array + { + $qb = $this->buildQueryBySubscriber($user)->select('ew'); + + foreach ($orderBy as $key => $sort) { + $qb->addOrderBy('ew.' . $key, $sort); + } + + $qb->setMaxResults($limit)->setFirstResult($offset); + + return $qb->getQuery()->getResult(); + } + + public function findOneBy(array $criteria): ?EntityWorkflow + { + return $this->repository->findOneBy($criteria); + } + + public function getClassName(): string + { + return EntityWorkflow::class; + } + + private function buildQueryByDest(User $user): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('ew'); + + $qb->join('ew.steps', 'step'); + + $qb->where( + $qb->expr()->andX( + $qb->expr()->isMemberOf(':user', 'step.destUser'), + $qb->expr()->isNull('step.transitionAfter') + ) + ); + + $qb->setParameter('user', $user); + + return $qb; + } + + private function buildQueryBySubscriber(User $user): QueryBuilder + { + $qb = $this->repository->createQueryBuilder('ew'); + + $qb->where( + $qb->expr()->orX( + $qb->expr()->isMemberOf(':user', 'ew.subscriberToStep'), + $qb->expr()->isMemberOf(':user', 'ew.subscriberToFinal'), + ) + ); + + $qb->setParameter('user', $user); + + return $qb; + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss index 4829f9634..39f8b3f8a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/chillmain.scss @@ -1,5 +1,5 @@ // Access to Bootstrap variables and mixins -@import '~ChillMainAssets/module/bootstrap/shared'; +@import 'ChillMainAssets/module/bootstrap/shared'; // Chill variables @import './scss/chill_variables'; @@ -277,11 +277,17 @@ table.table-bordered { } } +/// meta-data +div.updatedBy, +div.metadata { + span.user, span.date { + text-decoration: underline dotted; + } +} div.metadata { font-size: smaller; color: $gray-600; span.user, span.date { - text-decoration: underline dotted; &:hover { color: $gray-700; } @@ -424,7 +430,63 @@ span.item-key { //text-decoration: dotted underline; } +/// Workflows +div.workflow { + section.step { + border: 1px solid $chill-l-gray; + padding: 1em 2em; + div.flex-table { + margin: 1.5em -2em; + } + } + div.to-decision, + div.decided { + font-variant: all-small-caps; + margin-left: 1em; + } + div.to-decision { + font-weight: 300; + } + div.decided { + font-weight: 600; + } + div.breadcrumb { + display: initial; + margin-bottom: 0; + padding-right: 0.5em; + background-color: tint-color($chill-yellow, 90%); + border: 1px solid $chill-yellow; + color: $primary; + border-radius: 1.5em; + font-size: 12pt; + font-weight: 500; + font-variant: small-caps; + span, a { + cursor: pointer; + text-decoration: none; + &:hover { + font-weight: 700; + } + } + } +} + +// Override bootstrap popover styles +div.popover { + box-shadow: 0 0 10px -5px $dark; + .popover-arrow {} + .popover-header {} + .popover-body {} + + // Specific worflow breadcrumb popover + &.workflow-transition { + .popover-header { + font-variant: small-caps; + } + } +} + // increase toast message z-index (above all modals) div.v-toast { z-index: 10000!important; -} \ No newline at end of file +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss index ad0584d74..1eb437a2a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/buttons.scss @@ -18,6 +18,7 @@ $chill-theme-buttons: ( "show": $chill-blue, "view": $chill-blue, "misc": $gray-300, + "download": $gray-300, "cancel": $gray-300, "choose": $gray-300, "notify": $gray-300, @@ -78,6 +79,7 @@ $chill-theme-buttons: ( &.btn-choose::before, &.btn-notify::before, &.btn-tpchild::before, + &.btn-download::before, &.btn-cancel::before { font: normal normal normal 14px/1 ForkAwesome; margin-right: 0.5em; @@ -105,6 +107,7 @@ $chill-theme-buttons: ( &.btn-unlink::before { content: "\f127"; } // fa-chain-broken &.btn-notify::before { content: "\f1d8"; } // fa-paper-plane &.btn-tpchild::before { content: "\f007"; } // fa-user + &.btn-download::before { content: "\f019"; } // fa-download } diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss index 20fe927a6..67160863d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/flex_table.scss @@ -41,6 +41,15 @@ div.flex-table { margin-right: 5px; } } + + div.item-meta { + flex-grow: 1 !important; + flex-shrink: 1 !important; + width: unset !important; + display: flex; + flex-direction: column; + justify-content: center; + } } /* diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss index 93e9bd152..15f6deb3a 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/notification.scss @@ -68,6 +68,7 @@ div.notification-show { } // Override bootstrap accordion +div#workflow-fold, div#notification-fold { .accordion-button { padding: 0; @@ -78,3 +79,14 @@ div#notification-fold { } } } + +// Counter +div.notification-counter { + span { + &:not(:first-child) { + &::before { + content: '/ '; + } + } + } +} diff --git a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss index 6100bc842..37d4f97c4 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss +++ b/src/Bundle/ChillMainBundle/Resources/public/chill/scss/render_box.scss @@ -106,6 +106,8 @@ section.chill-entity { // used for comment-embeddable &.entity-comment-embeddable { width: 100%; + + /* already defined !! div.metadata { font-size: smaller; color: $gray-600; @@ -116,5 +118,6 @@ section.chill-entity { } } } + */ } } diff --git a/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js new file mode 100644 index 000000000..a89dd66f5 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/lib/entity-workflow/api.js @@ -0,0 +1,12 @@ +const buildLinkCreate = function(workflowName, relatedEntityClass, relatedEntityId) { + let params = new URLSearchParams(); + params.set('entityClass', relatedEntityClass); + params.set('entityId', relatedEntityId); + params.set('workflow', workflowName); + + return `/fr/main/workflow/create?`+params.toString(); +}; + +export { + buildLinkCreate, +}; diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/bootstrap/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/bootstrap/index.js index 936587dec..a9d34e01d 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/bootstrap/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/bootstrap/index.js @@ -9,9 +9,10 @@ import Dropdown from 'bootstrap/js/src/dropdown'; import Modal from 'bootstrap/js/dist/modal'; import Collapse from 'bootstrap/js/src/collapse'; import Carousel from 'bootstrap/js/src/carousel'; +import Popover from 'bootstrap/js/src/popover'; // -// ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section +// Carousel: ACHeaderSlider is a small slider used in banner of AccompanyingCourse Section // Initialize options, and show/hide controls in first/last slides // let ACHeaderSlider = document.querySelector('#ACHeaderSlider'); @@ -48,3 +49,14 @@ if (ACHeaderSlider) { } }) } + +// +// Popover: used in workflow breadcrumb, +// (expected in: contextual help, notification-box, workflow-box ) +// +const triggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]')); +const popoverList = triggerList.map(function (el) { + return new Popover(el, { + html: true, + }); +}); \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-pick/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-pick/index.js new file mode 100644 index 000000000..d6d657719 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-pick/index.js @@ -0,0 +1,49 @@ +import { createApp } from "vue"; +import PickWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/PickWorkflow.vue'; +import ListWorkflowVue from 'ChillMainAssets/vuejs/_components/EntityWorkflow/ListWorkflow.vue'; + +// pick workflow +document.querySelectorAll('[data-pick-workflow]') + .forEach(function(el) { + const app = { + components: { + PickWorkflowVue + }, + template: + '', + data() { + return { + relatedEntityClass: el.dataset.relatedEntityClass, + relatedEntityId: Number.parseInt(el.dataset.relatedEntityId), + workflowsAvailables: JSON.parse(el.dataset.workflowsAvailables), + } + } + }; + createApp(app).mount(el); + }) +; + +// list workflow +document.querySelectorAll('[data-list-workflows]') + .forEach(function (el) { + const app = { + components: { + ListWorkflowVue, + }, + template: + '', + data() { + return { + workflows: JSON.parse(el.dataset.workflows), + } + } + }; + createApp(app).mount(el); + }) +; \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-subscribe/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-subscribe/index.js new file mode 100644 index 000000000..c0e482aab --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/entity-workflow-subscribe/index.js @@ -0,0 +1,32 @@ +import {createApp} from "vue"; +import EntityWorkflowVueSubscriber from 'ChillMainAssets/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue'; +import { _createI18n } from 'ChillMainAssets/vuejs/_js/i18n'; +import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n'; + +const i18n = _createI18n(appMessages); + +let containers = document.querySelectorAll('[data-entity-workflow-subscribe]'); + +containers.forEach(container => { + let app = { + components: { + EntityWorkflowVueSubscriber, + }, + template: '', + data() { + return { + entityWorkflowId: Number.parseInt(container.dataset.entityWorkflowId), + subscriberStep: container.dataset.subscribeStep === "1", + subscriberFinal: container.dataset.subscribeFinal === "1", + } + }, + methods: { + onUpdate(status) { + this.subscriberStep = status.step; + this.subscriberFinal = status.final; + } + } + } + + createApp(app).use(i18n).mount(container); +}) diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js index 83a890cd9..329ac4e6c 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js +++ b/src/Bundle/ChillMainBundle/Resources/public/module/pick-entity/index.js @@ -31,7 +31,7 @@ window.addEventListener('DOMContentLoaded', function(e) { return { multiple: isMultiple, types: JSON.parse(el.dataset.types), - picked, + picked: picked === null ? [] : picked, uniqid: el.dataset.uniqid, } }, diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js b/src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js new file mode 100644 index 000000000..f39b6c83b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/module/wopi-link/index.js @@ -0,0 +1,29 @@ +import { createApp } from 'vue'; +import OpenWopiLink from 'ChillMainAssets/vuejs/_components/OpenWopiLink'; +import {_createI18n} from "ChillMainAssets/vuejs/_js/i18n"; + +const i18n = _createI18n({}); + +window.addEventListener('DOMContentLoaded', function (e) { + document.querySelectorAll('span[data-module="wopi-link"]') + .forEach(function (el) { + createApp({ + template: '', + components: { + OpenWopiLink + }, + data() { + return { + wopiUrl: el.dataset.wopiUrl, + title: el.dataset.docTitle, + type: el.dataset.docType, + button: el.dataset.button ? JSON.parse(el.dataset.button) : {} + } + } + }) + .use(i18n) + .mount(el) + ; + }) + ; +}); \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js new file mode 100644 index 000000000..2e2d4e89c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/page/workflow-show/index.js @@ -0,0 +1,30 @@ +import {ShowHide} from 'ChillMainAssets/lib/show_hide/show_hide.js'; + +window.addEventListener('DOMContentLoaded', function() { + let + finalizeAfterContainer = document.querySelector('#finalizeAfter'), + futureDestUsersContainer = document.querySelector('#futureDestUsers') + ; + + if (null === finalizeAfterContainer) { + return; + } + + new ShowHide({ + load_event: null, + froms: [finalizeAfterContainer], + container: [futureDestUsersContainer], + test: function(containers, arg2, arg3) { + for (let container of containers) { + for (let input of container.querySelectorAll('input')) { + if (!input.checked) { + return true; + } else { + return false; + } + } + } + + }, + }) +}); diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue new file mode 100644 index 000000000..3e3d5405e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/EntityWorkflowVueSubscriber.vue @@ -0,0 +1,101 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflow.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflow.vue new file mode 100644 index 000000000..4a3346972 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/ListWorkflow.vue @@ -0,0 +1,36 @@ + + + \ No newline at end of file diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue new file mode 100644 index 000000000..3a39c0eff --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/EntityWorkflow/PickWorkflow.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue index b806c4b3e..1a577274b 100644 --- a/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue +++ b/src/Bundle/ChillMainBundle/Resources/public/vuejs/_components/Modal.vue @@ -15,7 +15,7 @@ - diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig index 391158a00..5750a3a2c 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/_list_item.html.twig @@ -2,7 +2,7 @@ @@ -58,7 +58,7 @@
      • - {% if c.full_content is defined and c.full_content == 'true' %} + {% if c.full_content is defined and c.full_content == true %} {{ c.notification.message|chill_markdown_to_html }} {% else %} {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} @@ -66,7 +66,7 @@ {% endif %}
        - {% if c.action_button is not defined or c.action_button != 'false' %} + {% if c.action_button is not defined or c.action_button != false %}
        • @@ -102,7 +102,7 @@
          - {% if fold_item is defined and fold_item != 'false' %} + {% if fold_item is defined and fold_item != false %}
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig index 690f2187d..ed4a05c34 100644 --- a/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig +++ b/src/Bundle/ChillMainBundle/Resources/views/Notification/show.html.twig @@ -40,8 +40,8 @@ 'template': handler.getTemplate(notification), 'template_data': handler.getTemplateData(notification) }, - 'action_button': 'false', - 'full_content': 'true' + 'action_button': false, + 'full_content': true } %}
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig new file mode 100644 index 000000000..b825e4d43 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_attachment.html.twig @@ -0,0 +1,120 @@ +{# TODO Adapt condition #} +{% if random(1) == 0 %} + + {# For a document #} +

          {{ 'Document'|trans ~ 'target'|trans }}

          + +
          +
          + +
          +
          +

          Imprimé unique, parcours n°14635

          + Document PDF (6.2 Mo) +

          + Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor. +

          +
          +
          + +{% else %} + + {# For an action #} +

          {{ 'Accompanying Course Action'|trans ~ 'target'|trans }}

          + +
          + {# dynamic insertion + ::: TODO delete all static insertion, remove condition and pass work object in inclusion + #}{% if dynamic is defined %} + + {% set work = '' %} + {% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %} + + {% else %} + + {# BEGIN static insertion #} +
          +
          +

          + + Exercer un AEB > Conclure l'AEB +
            +
          • Date de début : 25/11/2021
          • +
          • Date de fin : 10/03/2022
          • +
          +
          +

          +
          +
          +
          +
          +

          Référent

          +

          Fred

          +
          +
          +

          Usagers du parcours

          + +
          +
          +

          Problématique sociale

          +
          + +
          +
          +
          +
          +
          + + + + + + + + + + + +

          Objectif - motif - dispositif

          Résultats - orientations

          +

          Aucun objectif - motif - dispositif

          +
          +
            +
          • Résultat : Arrêt à l'initiative du ménage pour déménagement
          • +
          • Orientation vers une MASP
          • +
          +
          +
          +
          +
          + Dernière mise à jour par + Fred(Responsable tous les territoires)(ASE),
          + le 3 décembre 2021 à 15:19 +
          +
          +
          + {# END static insertion #} + + {% endif %} +
          + +{% endif %} + +
            +
          • + +
          • +
          • + {% set x = random(1) %} + +
          • +
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_comment.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_comment.html.twig new file mode 100644 index 000000000..ec7cf8875 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_comment.html.twig @@ -0,0 +1,13 @@ +

          {{ 'Join a comment'|trans }}

          + +{{ form_start(comment_form) }} + +{{ form_widget(comment_form.comment) }} + +
            +
          • + +
          • +
          + +{{ form_end(comment_form) }} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig new file mode 100644 index 000000000..5751449c3 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_decision.html.twig @@ -0,0 +1,48 @@ +

          {{ 'Decision'|trans }}

          + +{% if transition_form is not null %} + {{ form_start(transition_form) }} + + {{ form_row(transition_form.transition) }} + +
          + {{ form_row(transition_form.finalizeAfter) }} +
          + + {% if transition_form.freezeAfter is defined %} + {{ form_row(transition_form.freezeAfter) }} + {% endif %} + +
          + {{ form_row(transition_form.future_dest_users) }} +
          + +

          {{ form_label(transition_form.comment) }}

          + + {{ form_widget(transition_form.comment) }} + +
            +
          • + +
          • +
          + + {{ form_end(transition_form) }} +{% else %} +
          + + {% if entity_workflow.currentStep.isFinalizeAfter %} +

          {{ 'workflow.This workflow is finalized'|trans }}

          + {% else %} +

          {{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}

          +

          {{ 'workflow.Only those users are allowed'|trans }}:

          + +
            + {% for u in entity_workflow.currentStep.destUser -%} +
          • {{ u|chill_entity_render_box }}
          • + {%- endfor %} +
          + {% endif %} +
          + +{% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_extension_list_workflow_for.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_extension_list_workflow_for.html.twig new file mode 100644 index 000000000..67037465b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_extension_list_workflow_for.html.twig @@ -0,0 +1,13 @@ +{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %} + {# vue component #} +
          +{% endif %} + +{% if entity_workflows|length > 0 %} + {# vue component #} +
          +{% endif %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_follow.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_follow.html.twig new file mode 100644 index 000000000..7a14e83fe --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_follow.html.twig @@ -0,0 +1,8 @@ +

          {{ 'Follow workflow'|trans }}

          + +{# vue component #} +
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig new file mode 100644 index 000000000..faa94e3a7 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_history.html.twig @@ -0,0 +1,59 @@ +

          {{ 'Workflow history'|trans }}

          + +
          + {% for step in entity_workflow.stepsChained %} +
          +
          + {% if loop.first and step.next is null %} +
          + {{ 'workflow.No transitions'|trans }} +
          + {% endif %} + +
          +
          + {% if not loop.first %} + + {% endif %} + {{ step.currentStep }} +
          + {# +
          + + Refusé +
          + #} +
          +
          + {% if step.next is not null %} +
          +
          + {% if step.transitionBy is not null %} +
          + {{ step.transitionBy|chill_entity_render_box }} +
          + {% endif %} +
          + {{ step.transitionAt|format_datetime('long', 'medium') }} +
          +
          +
          +
          + + {{ step.next.currentStep }} +
          +
          +
          + {% endif %} + {% if step.comment is not empty %} +
          +
          + {{ step.comment|chill_markdown_to_html }} +
          +
          + {% endif %} +
          + + {% endfor %} + +
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/_notification_include.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_notification_include.html.twig new file mode 100644 index 000000000..c20f51193 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/_notification_include.html.twig @@ -0,0 +1,12 @@ +
          +

          + {{ 'workflow_'|trans }} +

          + {% include handler.templateTitle(l.entity_workflow) with handler.templateTitleData(entity_workflow)|merge({ + 'description': true, + 'breadcrumb': true, + 'add_classes': 'ms-3 h3' + }) %} + + +
          diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig new file mode 100644 index 000000000..588e2de0b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/index.html.twig @@ -0,0 +1,56 @@ +{% extends '@ChillMain/layout.html.twig' %} + +{% block title %} + {{ 'Workflow'|trans }} +{% endblock %} + +{% block js %} + {{ parent() }} + + {{ encore_entry_script_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_pickentity_type') }} + {{ encore_entry_script_tags('mod_entity_workflow_subscribe') }} + {{ encore_entry_script_tags('page_workflow_show') }} + {{ encore_entry_script_tags('mod_wopi_link') }} +{% endblock %} + +{% block css %} + {{ parent() }} + {{ encore_entry_link_tags('mod_pickentity_type') }} + {{ encore_entry_link_tags('mod_entity_workflow_subscribe') }} + {{ encore_entry_link_tags('page_workflow_show') }} + {{ encore_entry_link_tags('mod_wopi_link') }} +{% endblock %} + +{% block content %} +
          +

          {{ block('title') }}

          + + {# handler_template: + - src/Bundle/ChillPersonBundle/Resources/views/Workflow/_evaluation.html.twig + - src/Bundle/ChillPersonBundle/Resources/views/Workflow/_accompanying_period_work.html.twig + - src/Bundle/ChillDocStoreBundle/Resources/views/AccompanyingCourseDocument/_workflow.html.twig + #} +
          +
          + {% include handler_template_title with handler_template_data|merge({'breadcrumb': true }) %} +
          + {% include handler_template with handler_template_data|merge({'display_action': true }) %} +
          + +
          {% include '@ChillMain/Workflow/_follow.html.twig' %}
          +
          {% include '@ChillMain/Workflow/_decision.html.twig' %}
          {# +
          {% include '@ChillMain/Workflow/_comment.html.twig' %}
          #} +
          {% include '@ChillMain/Workflow/_history.html.twig' %}
          + + {# useful ? + + #} +
          +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/list.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/list.html.twig new file mode 100644 index 000000000..6da68e390 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/list.html.twig @@ -0,0 +1,97 @@ +{% extends 'ChillMainBundle::layout.html.twig' %} + +{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as macro %} + +{% block title %} + {{ 'workflow.My workflows'|trans }} +{% endblock %} + +{% block content %} +
          + +

          {{ block('title') }}

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

          {{ 'workflow.No workflow'|trans }}

          + {% else %} +
          + {% for l in workflows %} +
          +
          + + {{ macro.breadcrumb(l) }} + +
          +
          + +
          + {% include l.handler.template(l.entity_workflow) with l.handler.templateData(l.entity_workflow)|merge({ + 'display_action': false + }) %} +
          +
          +
          +

          + {% if l.entity_workflow.isUserSubscribedToStep(app.user) %} + + {{ 'workflow.you subscribed to all steps'|trans }} + {% endif %} +

          +

          + {% if l.entity_workflow.isUserSubscribedToFinal(app.user) %} + + {{ 'workflow.you subscribed to final step'|trans }} + {% endif %} +

          +
          + +
          + +
          +
          + {% endfor %} +
          + {% endif %} +
          +{% endblock %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig new file mode 100644 index 000000000..436394012 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/macro_breadcrumb.html.twig @@ -0,0 +1,45 @@ +{% macro popoverContent(step) %} +
            +
          • + {{ 'By'|trans ~ ' : ' }} + {{ step.transitionBy|chill_entity_render_box }} +
          • +
          • + {{ 'Le'|trans ~ ' : ' }} + {{ step.transitionAt|format_datetime('short', 'short') }} +
          • +
          +{% endmacro %} + +{% macro breadcrumb(_ctx) %} + +{% endmacro %} diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig new file mode 100644 index 000000000..1e8469968 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig @@ -0,0 +1,13 @@ +{{ dest.label }}, + +Un suivi "{{ workflow.text }}" a atteint une nouvelle étape: {{ workflow.text }} +{%- if is_dest %} + +Vous êtes invités à valider cette étape au plus tôt. +{% endif %} + +Vous pouvez visualiser le workflow sur cette page: + +{{ absolute_url(path('chill_main_workflow_show', {'id': entity_workflow.id})) }} + +Cordialement, diff --git a/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig new file mode 100644 index 000000000..4960b6138 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Resources/views/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig @@ -0,0 +1,5 @@ +{%- if is_dest -%} +Un suivi {{ workflow.text }} demande votre attention +{%- else -%} +Un suivi {{ workflow.text }} a atteint une nouvelle étape: {{ place.text }} +{%- endif -%} diff --git a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php index ae2eb1f2c..9b6d89e83 100644 --- a/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php +++ b/src/Bundle/ChillMainBundle/Routing/MenuBuilder/UserMenuBuilder.php @@ -69,6 +69,15 @@ class UserMenuBuilder implements LocalMenuBuilderInterface 'counter' => $nbNotifications, ]); + $menu + ->addChild( + $this->translator->trans('workflow.My workflows'), + ['route' => 'chill_main_workflow_list_dest'] + ) + ->setExtras([ + 'order' => 700, + ]); + $menu ->addChild( 'Change password', diff --git a/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php new file mode 100644 index 000000000..9542d3acb --- /dev/null +++ b/src/Bundle/ChillMainBundle/Security/Authorization/EntityWorkflowVoter.php @@ -0,0 +1,70 @@ +manager = $manager; + $this->security = $security; + } + + protected function supports($attribute, $subject) + { + return $subject instanceof EntityWorkflow && in_array($attribute, self::getRoles(), true); + } + + protected function voteOnAttribute($attribute, $subject, TokenInterface $token) + { + switch ($attribute) { + case self::CREATE: + case self::SEE: + $handler = $this->manager->getHandler($subject); + + $entityAttribute = $handler->getRoleShow($subject); + + if (null === $entityAttribute) { + return true; + } + + return $this->security->isGranted($entityAttribute, $handler->getRelatedEntity($subject)); + + default: + throw new UnexpectedValueException("attribute {$attribute} not supported"); + } + } + + private static function getRoles(): array + { + return [ + self::SEE, + self::CREATE, + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php new file mode 100644 index 000000000..c89101c5b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Tests/Entity/Workflow/EntityWorkflowTest.php @@ -0,0 +1,81 @@ +getCurrentStep()->setFinalizeAfter(true); + $entityWorkflow->setStep('final'); + + $this->assertTrue($entityWorkflow->isFinalize()); + } + + public function testIsFinalizeWith4Steps() + { + $entityWorkflow = new EntityWorkflow(); + + $this->assertFalse($entityWorkflow->isFinalize()); + + $entityWorkflow->setStep('two'); + + $this->assertFalse($entityWorkflow->isFinalize()); + + $entityWorkflow->setStep('previous_final'); + + $this->assertFalse($entityWorkflow->isFinalize()); + + $entityWorkflow->getCurrentStep()->setFinalizeAfter(true); + $entityWorkflow->setStep('final'); + + $this->assertTrue($entityWorkflow->isFinalize()); + } + + public function testIsFreeze() + { + $entityWorkflow = new EntityWorkflow(); + + $this->assertFalse($entityWorkflow->isFreeze()); + + $entityWorkflow->setStep('step_one'); + + $this->assertFalse($entityWorkflow->isFreeze()); + + $entityWorkflow->setStep('step_three'); + + $this->assertFalse($entityWorkflow->isFreeze()); + + $entityWorkflow->getCurrentStep()->setFreezeAfter(true); + + $this->assertFalse($entityWorkflow->isFreeze()); + + $entityWorkflow->setStep('freezed'); + + $this->assertTrue($entityWorkflow->isFreeze()); + + $entityWorkflow->setStep('after_freeze'); + + $this->assertTrue($entityWorkflow->isFreeze()); + + $this->assertTrue($entityWorkflow->getCurrentStep()->isFreezeAfter()); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php new file mode 100644 index 000000000..2e97f77af --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EntityWorkflowHandlerInterface.php @@ -0,0 +1,39 @@ +handlers = $handlers; + $this->registry = $registry; + } + + public function getHandler(EntityWorkflow $entityWorkflow, array $options = []): EntityWorkflowHandlerInterface + { + foreach ($this->handlers as $handler) { + if ($handler->supports($entityWorkflow, $options)) { + return $handler; + } + } + + throw new HandlerNotFoundException(); + } + + public function getSupportedWorkflows(EntityWorkflow $entityWorkflow): array + { + return $this->registry->all($entityWorkflow); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php new file mode 100644 index 000000000..fba36924a --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/EntityWorkflowTransitionEventSubscriber.php @@ -0,0 +1,114 @@ +chillLogger = $chillLogger; + $this->security = $security; + $this->userRender = $userRender; + } + + public static function getSubscribedEvents(): array + { + return [ + 'workflow.transition' => 'onTransition', + 'workflow.guard' => [ + ['guardEntityWorkflow', 0], + ], + ]; + } + + public function guardEntityWorkflow(GuardEvent $event) + { + if (!$event->getSubject() instanceof EntityWorkflow) { + return; + } + + /** @var EntityWorkflow $entityWorkflow */ + $entityWorkflow = $event->getSubject(); + + if ($entityWorkflow->isFinalize()) { + $event->addTransitionBlocker( + new TransitionBlocker( + 'workflow.The workflow is finalized', + 'd6306280-7535-11ec-a40d-1f7bee26e2c0' + ) + ); + + return; + } + + if (!$entityWorkflow->getCurrentStep()->getDestUser()->contains($this->security->getUser())) { + if (!$event->getMarking()->has('initial')) { + $event->addTransitionBlocker(new TransitionBlocker( + 'workflow.You are not allowed to apply a transition on this workflow. Only those users are allowed: %users%', + 'f3eeb57c-7532-11ec-9495-e7942a2ac7bc', + [ + '%users%' => implode( + ', ', + $entityWorkflow->getCurrentStep()->getDestUser()->map(function (User $u) { + return $this->userRender->renderString($u, []); + })->toArray() + ), + ] + )); + } + } + } + + public function onTransition(Event $event) + { + if (!$event->getSubject() instanceof EntityWorkflow) { + return; + } + + /** @var EntityWorkflow $entityWorkflow */ + $entityWorkflow = $event->getSubject(); + $step = $entityWorkflow->getCurrentStep(); + + $step + ->setTransitionAfter($event->getTransition()->getName()) + ->setTransitionAt(new DateTimeImmutable('now')) + ->setTransitionBy($this->security->getUser()); + + $this->chillLogger->info('[workflow] apply transition on entityWorkflow', [ + 'relatedEntityClass' => $entityWorkflow->getRelatedEntityClass(), + 'relatedEntityId' => $entityWorkflow->getRelatedEntityId(), + 'transition' => $event->getTransition()->getName(), + 'by_user' => $this->security->getUser(), + 'entityWorkflow' => $entityWorkflow->getId(), + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php new file mode 100644 index 000000000..fd251bca6 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/EventSubscriber/NotificationOnTransition.php @@ -0,0 +1,103 @@ +entityManager = $entityManager; + $this->engine = $engine; + $this->metadataExtractor = $metadataExtractor; + $this->registry = $registry; + $this->security = $security; + } + + public static function getSubscribedEvents(): array + { + return [ + 'workflow.completed' => 'onCompleted', + ]; + } + + public function onCompleted(Event $event): void + { + if (!$event->getSubject() instanceof EntityWorkflow) { + return; + } + + /** @var EntityWorkflow $entityWorkflow */ + $entityWorkflow = $event->getSubject(); + + $dests = array_merge( + $entityWorkflow->getSubscriberToStep()->toArray(), + $entityWorkflow->isFinalize() ? $entityWorkflow->getSubscriberToFinal()->toArray() : [], + $entityWorkflow->getCurrentStep()->getDestUser()->toArray() + ); + + $place = $this->metadataExtractor->buildArrayPresentationForPlace($entityWorkflow); + $workflow = $this->metadataExtractor->buildArrayPresentationForWorkflow( + $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()) + ); + + $visited = []; + + foreach ($dests as $subscriber) { + if ( + $this->security->getUser() === $subscriber + || in_array($subscriber->getId(), $visited, true) + ) { + continue; + } + + $context = [ + 'entity_workflow' => $entityWorkflow, + 'dest' => $subscriber, + 'place' => $place, + 'workflow' => $workflow, + 'is_dest' => $entityWorkflow->getCurrentStep()->getDestUser()->contains($subscriber), + ]; + + $notification = new Notification(); + $notification + ->setRelatedEntityId($entityWorkflow->getId()) + ->setRelatedEntityClass(EntityWorkflow::class) + ->setTitle($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_title.fr.txt.twig', $context)) + ->setMessage($this->engine->render('@ChillMain/Workflow/workflow_notification_on_transition_completed_content.fr.txt.twig', $context)) + ->addAddressee($subscriber); + $this->entityManager->persist($notification); + + $visited[] = $subscriber->getId(); + } + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Exception/HandlerNotFoundException.php b/src/Bundle/ChillMainBundle/Workflow/Exception/HandlerNotFoundException.php new file mode 100644 index 000000000..99ad5b88c --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Exception/HandlerNotFoundException.php @@ -0,0 +1,18 @@ +registry = $registry; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function availableWorkflowFor(string $relatedEntityClass, ?int $relatedEntityId = 0): array + { + $blankEntityWorkflow = new EntityWorkflow(); + $blankEntityWorkflow + ->setRelatedEntityId($relatedEntityId) + ->setRelatedEntityClass($relatedEntityClass); + + // build the list of available workflows, and extract their names from metadata + $workflows = $this->registry->all($blankEntityWorkflow); + $workflowsList = []; + + foreach ($workflows as $workflow) { + $metadata = $workflow->getMetadataStore()->getWorkflowMetadata(); + $text = array_key_exists('label', $metadata) ? + $this->translatableStringHelper->localize($metadata['label']) : $workflow->getName(); + + $workflowsList[] = ['name' => $workflow->getName(), 'text' => $text]; + } + + return $workflowsList; + } + + public function buildArrayPresentationForPlace(EntityWorkflow $entityWorkflow): array + { + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + + $markingMetadata = $workflow->getMetadataStore()->getPlaceMetadata($entityWorkflow->getCurrentStep()->getCurrentStep()); + + $text = array_key_exists('label', $markingMetadata) ? + $this->translatableStringHelper->localize($markingMetadata['label']) : $entityWorkflow->getCurrentStep()->getCurrentStep(); + + return ['name' => $entityWorkflow->getCurrentStep()->getCurrentStep(), 'text' => $text]; + } + + public function buildArrayPresentationForWorkflow(WorkflowInterface $workflow): array + { + $metadata = $workflow->getMetadataStore()->getWorkflowMetadata(); + $text = array_key_exists('label', $metadata) ? + $this->translatableStringHelper->localize($metadata['label']) : $workflow->getName(); + + return ['name' => $workflow->getName(), 'text' => $text]; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Notification/WorkflowNotificationHandler.php b/src/Bundle/ChillMainBundle/Workflow/Notification/WorkflowNotificationHandler.php new file mode 100644 index 000000000..756d79fbf --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Notification/WorkflowNotificationHandler.php @@ -0,0 +1,45 @@ +entityWorkflowRepository->find($notification->getRelatedEntityId()); + + return [ + 'entity_workflow' => $entityWorkflow, + 'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow), + ]; + } + + public function supports(Notification $notification, array $options = []): bool + { + return $notification->getRelatedEntityClass() === EntityWorkflow::class; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/RelatedEntityWorkflowSupportsStrategy.php b/src/Bundle/ChillMainBundle/Workflow/RelatedEntityWorkflowSupportsStrategy.php new file mode 100644 index 000000000..310c0126b --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/RelatedEntityWorkflowSupportsStrategy.php @@ -0,0 +1,35 @@ +getMetadataStore()->getWorkflowMetadata()['related_entity'] + as $relatedEntityClass) { + if ($subject->getRelatedEntityClass() === $relatedEntityClass) { + return true; + } + } + + return false; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtension.php b/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtension.php new file mode 100644 index 000000000..09dce0c6e --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtension.php @@ -0,0 +1,29 @@ + true, 'is_safe' => ['html']] + ), + ]; + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtensionRuntime.php b/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtensionRuntime.php new file mode 100644 index 000000000..0f034267f --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Templating/WorkflowTwigExtensionRuntime.php @@ -0,0 +1,79 @@ +entityWorkflowManager = $entityWorkflowManager; + $this->registry = $registry; + $this->repository = $repository; + $this->metadataExtractor = $metadataExtractor; + $this->normalizer = $normalizer; + } + + public function listWorkflows(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string + { + $blankEntityWorkflow = new EntityWorkflow(); + $blankEntityWorkflow + ->setRelatedEntityId($relatedEntityId) + ->setRelatedEntityClass($relatedEntityClass); + + $workflowsList = $this->metadataExtractor->availableWorkflowFor($relatedEntityClass, $relatedEntityId); + + // get the related entity already created + $entityWorkflows = []; + + foreach ($entityWorkflowsNaked = $this->repository->findBy( + ['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId] + ) as $entityWorkflow) { + $workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName()); + $entityWorkflows[] = [ + 'entity_workflow' => $entityWorkflow, + 'workflow' => $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow), + 'handler' => $this->entityWorkflowManager->getHandler($entityWorkflow), + ]; + } + + return $environment->render('@ChillMain/Workflow/_extension_list_workflow_for.html.twig', [ + 'entity_workflows_json' => $this->normalizer->normalize($entityWorkflowsNaked, 'json', ['groups' => 'read']), + 'entity_workflows' => $entityWorkflows, + 'blank_workflow' => $blankEntityWorkflow, + 'workflows_availables' => $workflowsList, + ]); + } +} diff --git a/src/Bundle/ChillMainBundle/Workflow/Validator/EntityWorkflowCreation.php b/src/Bundle/ChillMainBundle/Workflow/Validator/EntityWorkflowCreation.php new file mode 100644 index 000000000..dc01f9019 --- /dev/null +++ b/src/Bundle/ChillMainBundle/Workflow/Validator/EntityWorkflowCreation.php @@ -0,0 +1,35 @@ +entityWorkflowManager = $entityWorkflowManager; + } + + /** + * @param EntityWorkflow $value + * @param Constraint|EntityWorkflowCreation $constraint + */ + public function validate($value, Constraint $constraint) + { + if (!$value instanceof EntityWorkflow) { + throw new UnexpectedValueException($value, EntityWorkflow::class); + } + + if (!$constraint instanceof EntityWorkflowCreation) { + throw new UnexpectedTypeException($constraint, EntityWorkflowCreation::class); + } + + try { + $handler = $this->entityWorkflowManager->getHandler($value); + } catch (HandlerNotFoundException $e) { + $this->context->buildViolation($constraint->messageHandlerNotFound) + ->addViolation(); + + return; + } + + if (null === $handler->getRelatedEntity($value)) { + $this->context->buildViolation($constraint->messageEntityNotFound) + ->addViolation(); + } + + $workflows = $this->entityWorkflowManager->getSupportedWorkflows($value); + + $matched = array_filter($workflows, static function (WorkflowInterface $workflow) use ($value) { + return $workflow->getName() === $value->getWorkflowName(); + }); + + if (0 === count($matched)) { + $this->context->buildViolation($constraint->messageWorkflowNotAvailable) + ->addViolation(); + } + } +} diff --git a/src/Bundle/ChillMainBundle/chill.webpack.config.js b/src/Bundle/ChillMainBundle/chill.webpack.config.js index 47aa32865..113d326c1 100644 --- a/src/Bundle/ChillMainBundle/chill.webpack.config.js +++ b/src/Bundle/ChillMainBundle/chill.webpack.config.js @@ -52,6 +52,7 @@ module.exports = function(encore, entries) // Page entrypoints encore.addEntry('page_login', __dirname + '/Resources/public/page/login/index.js'); encore.addEntry('page_location', __dirname + '/Resources/public/page/location/index.js'); + encore.addEntry('page_workflow_show', __dirname + '/Resources/public/page/workflow-show/index.js'); buildCKEditor(encore); @@ -64,6 +65,9 @@ module.exports = function(encore, entries) encore.addEntry('mod_input_address', __dirname + '/Resources/public/vuejs/Address/mod_input_address_index.js'); encore.addEntry('mod_notification_toggle_read_status', __dirname + '/Resources/public/module/notification/toggle_read.js'); encore.addEntry('mod_pickentity_type', __dirname + '/Resources/public/module/pick-entity/index.js'); + encore.addEntry('mod_entity_workflow_subscribe', __dirname + '/Resources/public/module/entity-workflow-subscribe/index.js'); + encore.addEntry('mod_entity_workflow_pick', __dirname + '/Resources/public/module/entity-workflow-pick/index.js'); + encore.addEntry('mod_wopi_link', __dirname + '/Resources/public/module/wopi-link/index.js'); // Vue entrypoints encore.addEntry('vue_address', __dirname + '/Resources/public/vuejs/Address/index.js'); diff --git a/src/Bundle/ChillMainBundle/config/services.yaml b/src/Bundle/ChillMainBundle/config/services.yaml index adf38988e..dd2407d2b 100644 --- a/src/Bundle/ChillMainBundle/config/services.yaml +++ b/src/Bundle/ChillMainBundle/config/services.yaml @@ -26,6 +26,20 @@ services: tags: - { name: 'doctrine.event_subscriber' } + # workflow related + Chill\MainBundle\Workflow\: + resource: '../Workflow/' + autowire: true + autoconfigure: true + + Chill\MainBundle\Workflow\EntityWorkflowManager: + autoconfigure: true + autowire: true + arguments: + $handlers: !tagged_iterator chill_main.workflow_handler + + # other stuffes + chill.main.helper.translatable_string: class: Chill\MainBundle\Templating\TranslatableStringHelper diff --git a/src/Bundle/ChillMainBundle/config/services/form.yaml b/src/Bundle/ChillMainBundle/config/services/form.yaml index 0d0a17201..7efc9a69f 100644 --- a/src/Bundle/ChillMainBundle/config/services/form.yaml +++ b/src/Bundle/ChillMainBundle/config/services/form.yaml @@ -141,3 +141,5 @@ services: autoconfigure: true Chill\MainBundle\Form\Type\LocationFormType: ~ + + Chill\MainBundle\Form\WorkflowStepType: ~ diff --git a/src/Bundle/ChillMainBundle/config/services/security.yaml b/src/Bundle/ChillMainBundle/config/services/security.yaml index 30eed05c0..b884a92fc 100644 --- a/src/Bundle/ChillMainBundle/config/services/security.yaml +++ b/src/Bundle/ChillMainBundle/config/services/security.yaml @@ -26,6 +26,8 @@ services: Chill\MainBundle\Security\Authorization\NotificationVoter: ~ + Chill\MainBundle\Security\Authorization\EntityWorkflowVoter: ~ + Chill\MainBundle\Security\Authorization\VoterHelperFactoryInterface: '@Chill\MainBundle\Security\Authorization\DefaultVoterHelperFactory' chill.main.security.authorization.helper: diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220112123436.php b/src/Bundle/ChillMainBundle/migrations/Version20220112123436.php new file mode 100644 index 000000000..7358a3b4b --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220112123436.php @@ -0,0 +1,72 @@ +addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final DROP CONSTRAINT FK_C2CE504C7D99CE94'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step DROP CONSTRAINT FK_ECB8F5417D99CE94'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP CONSTRAINT FK_440AA6FEFB054143'); + $this->addSql('ALTER TABLE chill_main_entity_workflow_step_user DROP CONSTRAINT FK_A9F001FA7E6AF9D4'); + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_workflow_entity'); + $this->addSql('DROP TABLE chill_main_workflow_entity_subscriber_to_final'); + $this->addSql('DROP TABLE chill_main_workflow_entity_subscriber_to_step'); + $this->addSql('DROP TABLE chill_main_workflow_entity_step'); + $this->addSql('DROP TABLE chill_main_entity_workflow_step_user'); + } + + public function getDescription(): string + { + return 'Create tables for workflow'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_main_workflow_entity_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE chill_main_workflow_entity (id INT NOT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, relatedEntityClass VARCHAR(255) NOT NULL, relatedEntityId INT NOT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, workflowName TEXT NOT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_5F087D553174800F ON chill_main_workflow_entity (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_5F087D5565FF1AEC ON chill_main_workflow_entity (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE chill_main_workflow_entity_subscriber_to_final (entityworkflow_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflow_id, user_id))'); + $this->addSql('CREATE INDEX IDX_C2CE504C7D99CE94 ON chill_main_workflow_entity_subscriber_to_final (entityworkflow_id)'); + $this->addSql('CREATE INDEX IDX_C2CE504CA76ED395 ON chill_main_workflow_entity_subscriber_to_final (user_id)'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_subscriber_to_step (entityworkflow_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflow_id, user_id))'); + $this->addSql('CREATE INDEX IDX_ECB8F5417D99CE94 ON chill_main_workflow_entity_subscriber_to_step (entityworkflow_id)'); + $this->addSql('CREATE INDEX IDX_ECB8F541A76ED395 ON chill_main_workflow_entity_subscriber_to_step (user_id)'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_step (id INT NOT NULL, currentStep TEXT NOT NULL, destEmail JSON NOT NULL, finalizeAfter BOOLEAN DEFAULT \'false\' NOT NULL, freezeAfter BOOLEAN DEFAULT \'false\' NOT NULL, transitionAfter TEXT DEFAULT NULL, transitionByEmail TEXT DEFAULT NULL, transitionAt TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, entityWorkflow_id INT DEFAULT NULL, transitionBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_440AA6FEFB054143 ON chill_main_workflow_entity_step (entityWorkflow_id)'); + $this->addSql('CREATE INDEX IDX_440AA6FE8829EF37 ON chill_main_workflow_entity_step (transitionBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step.transitionAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('CREATE TABLE chill_main_entity_workflow_step_user (entityworkflowstep_id INT NOT NULL, user_id INT NOT NULL, PRIMARY KEY(entityworkflowstep_id, user_id))'); + $this->addSql('CREATE INDEX IDX_A9F001FA7E6AF9D4 ON chill_main_entity_workflow_step_user (entityworkflowstep_id)'); + $this->addSql('CREATE INDEX IDX_A9F001FAA76ED395 ON chill_main_entity_workflow_step_user (user_id)'); + $this->addSql('ALTER TABLE chill_main_workflow_entity ADD CONSTRAINT FK_5F087D553174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity ADD CONSTRAINT FK_5F087D5565FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final ADD CONSTRAINT FK_C2CE504C7D99CE94 FOREIGN KEY (entityworkflow_id) REFERENCES chill_main_workflow_entity (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_final ADD CONSTRAINT FK_C2CE504CA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step ADD CONSTRAINT FK_ECB8F5417D99CE94 FOREIGN KEY (entityworkflow_id) REFERENCES chill_main_workflow_entity (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_subscriber_to_step ADD CONSTRAINT FK_ECB8F541A76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD CONSTRAINT FK_440AA6FEFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD CONSTRAINT FK_440AA6FE8829EF37 FOREIGN KEY (transitionBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_entity_workflow_step_user ADD CONSTRAINT FK_A9F001FA7E6AF9D4 FOREIGN KEY (entityworkflowstep_id) REFERENCES chill_main_workflow_entity_step (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_entity_workflow_step_user ADD CONSTRAINT FK_A9F001FAA76ED395 FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220114132105.php b/src/Bundle/ChillMainBundle/migrations/Version20220114132105.php new file mode 100644 index 000000000..2e7d7a99c --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220114132105.php @@ -0,0 +1,47 @@ +addSql('DROP SEQUENCE chill_main_workflow_entity_comment_id_seq CASCADE'); + $this->addSql('DROP TABLE chill_main_workflow_entity_comment'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step DROP comment'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step_user RENAME TO chill_main_entity_workflow_step_user'); + } + + public function getDescription(): string + { + return 'Add comment to entity workflow'; + } + + public function up(Schema $schema): void + { + $this->addSql('CREATE SEQUENCE chill_main_workflow_entity_comment_id_seq INCREMENT BY 1 MINVALUE 1 START 1;'); + $this->addSql('CREATE TABLE chill_main_workflow_entity_comment (id INT NOT NULL, comment TEXT NOT NULL DEFAULT \'\', createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, createdBy_id INT DEFAULT NULL, entityWorkflow_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_2655252F3174800F ON chill_main_workflow_entity_comment (createdBy_id)'); + $this->addSql('CREATE INDEX IDX_2655252FFB054143 ON chill_main_workflow_entity_comment (entityWorkflow_id)'); + $this->addSql('CREATE INDEX IDX_2655252F65FF1AEC ON chill_main_workflow_entity_comment (updatedBy_id)'); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_comment.createdAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_comment.updatedAt IS \'(DC2Type:datetime_immutable)\''); + $this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252F3174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252FFB054143 FOREIGN KEY (entityWorkflow_id) REFERENCES chill_main_workflow_entity (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_comment ADD CONSTRAINT FK_2655252F65FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step ADD comment TEXT NOT NULL DEFAULT \'\''); + $this->addSql('ALTER TABLE chill_main_entity_workflow_step_user RENAME TO chill_main_workflow_entity_step_user'); + } +} diff --git a/src/Bundle/ChillMainBundle/migrations/Version20220114165950.php b/src/Bundle/ChillMainBundle/migrations/Version20220114165950.php new file mode 100644 index 000000000..5489d3f1c --- /dev/null +++ b/src/Bundle/ChillMainBundle/migrations/Version20220114165950.php @@ -0,0 +1,37 @@ +addSql('ALTER TABLE chill_main_workflow_entity_step ALTER transitionat SET NOT NULL'); + } + + public function getDescription(): string + { + return 'remove not null on transition at'; + } + + public function up(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER transitionat DROP NOT NULL'); + $this->addSql('ALTER TABLE chill_main_workflow_entity_step ALTER transitionat SET DEFAULT NULL'); + } +} diff --git a/src/Bundle/ChillMainBundle/translations/messages.fr.yml b/src/Bundle/ChillMainBundle/translations/messages.fr.yml index 434909d9f..5c214367c 100644 --- a/src/Bundle/ChillMainBundle/translations/messages.fr.yml +++ b/src/Bundle/ChillMainBundle/translations/messages.fr.yml @@ -36,8 +36,9 @@ Choose an user: Choisir un utilisateur "You are going to leave a page with unsubmitted data. Are you sure you want to leave ?": "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?" No value: Aucune information Last updated by: Dernière mise à jour par -Last updated on: Dernière mise à jour le on: "le " +Last updated on: Dernière mise à jour le +by_user: "par " Edit: Modifier Update: Mettre à jour @@ -354,6 +355,43 @@ For: Pour Created for: Créé pour Created by: Créé par + +# Workflows 💊 +Workflow: Workflow — chemin de décision +Workflow n°%id%: 'Workflow (n°%id%)' +workflow_: Workflow +target: ' (cible)' +Decision: Décision +Join a comment: Laisser un commentaire +Follow workflow: Suivre la décision +Workflow history: Historique de la décision + +workflow: + Created by: Créé par + Transition: Prochaine étape + dest for next steps: Utilisateurs qui valideront la prochaine étape + Freeze: Geler + The associated element will be freezed: L'élément associé sera gelé et ne pourra plus être modifié après cette décision. + Finalize: Étape finale + The workflow will be finalized: Le suivi est clôturé lors de cette décision. + No transitions: Aucune transition + Comment added: Commentaire ajouté + This workflow is finalized: Ce suivi est finalisé. + You are not allowed to apply a transition on this workflow: Vous n'êtes pas autorisé à appliquer une décision pour ce suivi + Only those users are allowed: Seuls ces utilisateurs sont autorisés + My workflows: Mes workflows + No workflow: Aucun workflow + Evaluation (n°%eval%): "Évaluation (n°%eval%)" + Document (n°%doc%): "Document (n°%doc%)" + Work (n°%w%): "Action d'accompagnement (n°%w%)" + subscribed: Souscrit + dest: Destinataire de l'étape finale + you subscribed to all steps: Vous recevrez une notification à chaque étape + you subscribed to final step: Vous recevrez une notification à l'étape finale + +Subscribe final: Recevoir une notification à l'étape finale +Subscribe all steps: Recevoir une notification à chaque étape + notification: Notification: Notification My own notifications: Mes notifications diff --git a/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php new file mode 100644 index 000000000..06634b3cc --- /dev/null +++ b/src/Bundle/ChillPersonBundle/Repository/AccompanyingPeriod/AccompanyingPeriodWorkEvaluationRepository.php @@ -0,0 +1,61 @@ +repository = $entityManager->getRepository(AccompanyingPeriodWorkEvaluation::class); + } + + public function find($id): ?AccompanyingPeriodWorkEvaluation + { + return $this->repository->find($id); + } + + /** + * @return array|AccompanyingPeriodWorkEvaluation[] + */ + public function findAll(): array + { + return $this->repository->findAll(); + } + + /** + * @param null|mixed $limit + * @param null|mixed $offset + * + * @return array|AccompanyingPeriodWorkEvaluation[] + */ + public function findBy(array $criteria, ?array $orderBy = null, $limit = null, $offset = null): array + { + return $this->repository->findBy($criteria, $orderBy, $limit, $offset); + } + + public function findOneBy(array $criteria): ?AccompanyingPeriodWorkEvaluation + { + return $this->findOneBy($criteria); + } + + public function getClassName() + { + return AccompanyingPeriodWorkEvaluation::class; + } +} diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue index b5e5d3221..e43608895 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourseWorkEdit/App.vue @@ -241,7 +241,17 @@
        -
          +
            +