From 5423de3bd95958160067fd5a0fb09c37d1f2705f Mon Sep 17 00:00:00 2001 From: Julie Lenaerts Date: Mon, 17 Jan 2022 14:23:54 +0100 Subject: [PATCH 01/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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/42] 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 ad983d80d2d1ed5e1c1ddb96ce1ce370cb6f7601 Mon Sep 17 00:00:00 2001 From: juminet Date: Wed, 19 Jan 2022 10:59:44 +0000 Subject: [PATCH 09/42] Issue374 documents in activity bug --- CHANGELOG.md | 2 ++ src/Bundle/ChillActivityBundle/Form/ActivityType.php | 1 + .../Resources/views/Activity/editAccompanyingCourse.html.twig | 2 +- .../Resources/views/Activity/editPerson.html.twig | 2 +- .../Resources/views/Activity/newPerson.html.twig | 2 +- .../Resources/views/Activity/showAccompanyingCourse.html.twig | 2 ++ .../Resources/views/Activity/showPerson.html.twig | 2 ++ src/Bundle/ChillActivityBundle/translations/messages.fr.yml | 2 +- .../Service/DocGenerator/AccompanyingPeriodWorkContext.php | 3 --- 9 files changed, 11 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c78c7a3c..cc67899f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to ## Unreleased +* [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) + * [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 * rewrite page which allow to select activity diff --git a/src/Bundle/ChillActivityBundle/Form/ActivityType.php b/src/Bundle/ChillActivityBundle/Form/ActivityType.php index 7c5e0e410..6f935ad99 100644 --- a/src/Bundle/ChillActivityBundle/Form/ActivityType.php +++ b/src/Bundle/ChillActivityBundle/Form/ActivityType.php @@ -307,6 +307,7 @@ class ActivityType extends AbstractType 'allow_add' => true, 'button_add_label' => 'activity.Insert a document', 'button_remove_label' => 'activity.Remove a document', + 'empty_collection_explain' => 'No documents', ]); } diff --git a/src/Bundle/ChillActivityBundle/Resources/views/Activity/editAccompanyingCourse.html.twig b/src/Bundle/ChillActivityBundle/Resources/views/Activity/editAccompanyingCourse.html.twig index 5ad95c327..b278c0300 100644 --- a/src/Bundle/ChillActivityBundle/Resources/views/Activity/editAccompanyingCourse.html.twig +++ b/src/Bundle/ChillActivityBundle/Resources/views/Activity/editAccompanyingCourse.html.twig @@ -15,7 +15,7 @@ {% block js %} {{ parent() }} - {{ encore_entry_link_tags('mod_async_upload') }} + {{ encore_entry_script_tags('mod_async_upload') }} {% 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 14/42] 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 3471bdec0db2a252ef0235b007c8128db7c21811 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jan 2022 10:17:22 +0100 Subject: [PATCH 15/42] accompanyingPeriod: add job --- .../Entity/AccompanyingPeriod.php | 22 ++++++++++ .../migrations/Version20220119091025.php | 40 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 06e4f0c8b..13a597efc 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -20,6 +20,7 @@ use Chill\MainBundle\Entity\HasScopesInterface; use Chill\MainBundle\Entity\Location; use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\User; +use Chill\MainBundle\Entity\UserJob; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; use Chill\PersonBundle\Entity\AccompanyingPeriod\Comment; @@ -112,6 +113,15 @@ class AccompanyingPeriod implements */ public const STEP_DRAFT = 'DRAFT'; + + /** + * @ORM\ManyToOne( + * targetEntity=UserJob::class + * ) + * @Groups({"read", "write"}) + */ + private ?UserJob $job = null; + /** * @ORM\ManyToOne( * targetEntity=Address::class @@ -394,6 +404,18 @@ class AccompanyingPeriod implements return $array; } + public function getJob(): ?UserJob + { + return $this->job; + } + + public function setJob(?UserJob $job): self + { + $this->job = $job; + + return $this; + } + public function addComment(Comment $comment): self { $this->comments[] = $comment; diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php b/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php new file mode 100644 index 000000000..a8f125fdf --- /dev/null +++ b/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php @@ -0,0 +1,40 @@ +addSql('ALTER TABLE chill_person_accompanying_period ADD job_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE chill_person_accompanying_period ADD CONSTRAINT FK_E260A868BE04EA9 FOREIGN KEY (job_id) REFERENCES chill_main_user_job (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('CREATE INDEX IDX_E260A868BE04EA9 ON chill_person_accompanying_period (job_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_accompanying_period DROP CONSTRAINT FK_E260A868BE04EA9'); + $this->addSql('DROP INDEX IDX_E260A868BE04EA9'); + $this->addSql('ALTER TABLE chill_person_accompanying_period DROP job_id'); + } +} From 9f7f6e33e8688c907408bcca4952ed832c32ae17 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jan 2022 10:34:33 +0100 Subject: [PATCH 16/42] user-job: make an endpoint for userJob + doc swagger --- .../ChillMainExtension.php | 20 ++++++++++++++++ .../ChillMainBundle/chill.api.specs.yaml | 23 +++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php index 772f4d8ee..27d70e725 100644 --- a/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php +++ b/src/Bundle/ChillMainBundle/DependencyInjection/ChillMainExtension.php @@ -391,6 +391,26 @@ class ChillMainExtension extends Extension implements ], ], ], + [ + 'class' => \Chill\MainBundle\Entity\UserJob::class, + 'name' => 'user_job', + 'base_path' => '/api/1.0/main/user-job', + 'base_role' => 'ROLE_USER', + 'actions' => [ + '_index' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + '_entity' => [ + 'methods' => [ + Request::METHOD_GET => true, + Request::METHOD_HEAD => true, + ], + ], + ], + ], [ 'controller' => \Chill\MainBundle\Controller\AddressReferenceAPIController::class, 'class' => \Chill\MainBundle\Entity\AddressReference::class, diff --git a/src/Bundle/ChillMainBundle/chill.api.specs.yaml b/src/Bundle/ChillMainBundle/chill.api.specs.yaml index 753cb855c..4f2e3648e 100644 --- a/src/Bundle/ChillMainBundle/chill.api.specs.yaml +++ b/src/Bundle/ChillMainBundle/chill.api.specs.yaml @@ -116,7 +116,15 @@ components: type: number minItems: 2 maxItems: 2 - + UserJob: + type: object + properties: + id: + type: integer + label: + type: object + type: + type: string paths: /1.0/search.json: @@ -771,5 +779,16 @@ paths: description: "accepted" 403: description: "unauthorized" - + /1.0/main/user-job.json: + get: + tags: + - user + summary: Return a list of all user jobs + responses: + 200: + description: "ok" + content: + application/json: + schema: + $ref: '#/components/schemas/UserJob' From 175fa7bf2ffd77bbd6e7df9a534b0f55399083bf Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jan 2022 11:09:02 +0100 Subject: [PATCH 17/42] php code fix --- .../Entity/AccompanyingPeriod.php | 41 +++++++++---------- .../migrations/Version20220119091025.php | 16 ++++---- 2 files changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php index 13a597efc..3d7d1a7b4 100644 --- a/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php +++ b/src/Bundle/ChillPersonBundle/Entity/AccompanyingPeriod.php @@ -113,15 +113,6 @@ class AccompanyingPeriod implements */ public const STEP_DRAFT = 'DRAFT'; - - /** - * @ORM\ManyToOne( - * targetEntity=UserJob::class - * ) - * @Groups({"read", "write"}) - */ - private ?UserJob $job = null; - /** * @ORM\ManyToOne( * targetEntity=Address::class @@ -208,6 +199,14 @@ class AccompanyingPeriod implements */ private $intensity = self::INTENSITY_OCCASIONAL; + /** + * @ORM\ManyToOne( + * targetEntity=UserJob::class + * ) + * @Groups({"read", "write"}) + */ + private ?UserJob $job = null; + /** * @var DateTime * @@ -404,18 +403,6 @@ class AccompanyingPeriod implements return $array; } - public function getJob(): ?UserJob - { - return $this->job; - } - - public function setJob(?UserJob $job): self - { - $this->job = $job; - - return $this; - } - public function addComment(Comment $comment): self { $this->comments[] = $comment; @@ -647,6 +634,11 @@ class AccompanyingPeriod implements return $this->intensity; } + public function getJob(): ?UserJob + { + return $this->job; + } + /** * Get the location, taking precedence into account. * @@ -1080,6 +1072,13 @@ class AccompanyingPeriod implements return $this; } + public function setJob(?UserJob $job): self + { + $this->job = $job; + + return $this; + } + /** * Set openingDate. * diff --git a/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php b/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php index a8f125fdf..c047b3dca 100644 --- a/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php +++ b/src/Bundle/ChillPersonBundle/migrations/Version20220119091025.php @@ -15,10 +15,17 @@ use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; /** - * Add userJob to AccompanyingPeriod + * Add userJob to AccompanyingPeriod. */ final class Version20220119091025 extends AbstractMigration { + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE chill_person_accompanying_period DROP CONSTRAINT FK_E260A868BE04EA9'); + $this->addSql('DROP INDEX IDX_E260A868BE04EA9'); + $this->addSql('ALTER TABLE chill_person_accompanying_period DROP job_id'); + } + public function getDescription(): string { return 'Add userJob to AccompanyingPeriod'; @@ -30,11 +37,4 @@ final class Version20220119091025 extends AbstractMigration $this->addSql('ALTER TABLE chill_person_accompanying_period ADD CONSTRAINT FK_E260A868BE04EA9 FOREIGN KEY (job_id) REFERENCES chill_main_user_job (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); $this->addSql('CREATE INDEX IDX_E260A868BE04EA9 ON chill_person_accompanying_period (job_id)'); } - - public function down(Schema $schema): void - { - $this->addSql('ALTER TABLE chill_person_accompanying_period DROP CONSTRAINT FK_E260A868BE04EA9'); - $this->addSql('DROP INDEX IDX_E260A868BE04EA9'); - $this->addSql('ALTER TABLE chill_person_accompanying_period DROP job_id'); - } } From 16c86daafb49e31473f15dd548c34d7f0afd4ef6 Mon Sep 17 00:00:00 2001 From: nobohan Date: Wed, 19 Jan 2022 13:22:43 +0100 Subject: [PATCH 18/42] accompanying course: add selector for accompanying course job --- .../public/vuejs/AccompanyingCourse/api.js | 13 ++--- .../components/Referrer.vue | 57 ++++++++++++++++++- .../vuejs/AccompanyingCourse/js/i18n.js | 4 ++ .../vuejs/AccompanyingCourse/store/index.js | 28 +++++++-- 4 files changed, 86 insertions(+), 16 deletions(-) diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js index 8e4134205..ab95229a2 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/api.js @@ -15,21 +15,15 @@ const getAccompanyingCourse = (id) => { }); }; -const getUsers = () => { - const url = `/api/1.0/main/user.json`; - - return fetchResults(url); -}; +const getUsers = () => fetchResults('/api/1.0/main/user.json'); const getReferrersSuggested = (course) => { const url = `/api/1.0/person/accompanying-course/${course.id}/referrers-suggested.json`; - return fetchResults(url); } -/* -* Endpoint -*/ +const getUserJobs = () => fetchResults('/api/1.0/main/user-job.json'); + const getSocialIssues = () => { const url = `/api/1.0/person/social-work/social-issue.json`; return fetch(url) @@ -54,4 +48,5 @@ export { getAccompanyingCourse, getUsers, getReferrersSuggested, + getUserJobs }; diff --git a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue index 0556e342a..f801ca355 100644 --- a/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue +++ b/src/Bundle/ChillPersonBundle/Resources/public/vuejs/AccompanyingCourse/components/Referrer.vue @@ -15,13 +15,33 @@ :searchable="true" :placeholder="$t('referrer.placeholder')" v-model="value" - v-bind:options="users" + :options="users" :select-label="$t('multiselect.select_label')" :deselect-label="$t('multiselect.deselect_label')" :selected-label="$t('multiselect.selected_label')" @select="updateReferrer"> + + + + + + + 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 40/42] 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 @@
    -
      +
        +