mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'master' into issue394_address_confidential
This commit is contained in:
commit
7c8b08c3a7
36
CHANGELOG.md
36
CHANGELOG.md
@ -13,18 +13,46 @@ and this project adheres to
|
||||
<!-- write down unreleased development here -->
|
||||
* [main] increase length of 4 Address fields (change to TEXT, no size limits) (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/277)
|
||||
* [main] Add confidential option for address, in edit and view (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/165)
|
||||
|
||||
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
|
||||
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
|
||||
|
||||
## Test releases
|
||||
|
||||
### test release 2021-01-26
|
||||
|
||||
* [parcours] comments truncated if too long + link added (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/406)
|
||||
* [person]: possibility to add person resources (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/382)
|
||||
* [person ressources]: module added
|
||||
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
|
||||
* dispatching list
|
||||
|
||||
|
||||
|
||||
### test release 2022-01-24
|
||||
|
||||
* [person] name suggestions within create person form when person is created departing from a search input (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/377)
|
||||
* [notification: formulaire création] descend la box avec la description dans le bas du formulaire
|
||||
* [notification for activity]: fix link to activity
|
||||
* [notification] add "URGENT" before accompanying course with emergency = true
|
||||
* [notification] add a "read more" button on system notification
|
||||
* [notification] add `[Chill]` in the subject of each notification, automatically
|
||||
* [notification] add a counter for notification in activity list and accompanying period list, and search results
|
||||
* [parcours] bugfix if deathdate is not defined (eg. for a thirdparty) parcours is still displayed. Gave error before.
|
||||
* [workflow] add breadcrumb to show steps
|
||||
* [popover] add popover html popup mechanism (used by workflow breadcrumb)
|
||||
* [templates] improve updatedBy macro in item metadatas
|
||||
* [parcours]: bug fix when comment is pinned all other comments remain in the collection (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/385)
|
||||
* [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-19
|
||||
* vuejs: add dead information on all on-the-fly person render boxes, in vis graph and other templates (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/271)
|
||||
* [thirdparty] fix bug in 3rd party view: types was replaced by thirdPartyTypes
|
||||
* [main] location form type: fix unmapped address field (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/246)
|
||||
* [activity] fix wrong import of js assets for adding and viewing documents in activity (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/83 & https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/176)
|
||||
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
|
||||
* [forms] dynamic picker types for user/person/thirdparty types created (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/386)
|
||||
|
||||
### test release 2022-01-17
|
||||
|
||||
@ -34,10 +62,14 @@ 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
|
||||
* vuejs: add validation on required fields for AddPerson, Address and Location components
|
||||
* vuejs: treat 422 validation errors in locations and AddPerson components
|
||||
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
|
||||
|
||||
## Test releases
|
||||
* vuejs: add validation on required fields for AddPerson, Address and Location components
|
||||
* vuejs: treat 422 validation errors in locations and AddPerson components
|
||||
* [person]: space added between deathdate and age in twig renderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/380)
|
||||
|
||||
### test release 2022-01-12
|
||||
|
||||
|
@ -52,9 +52,6 @@
|
||||
"twig/string-extra": "^3.3",
|
||||
"twig/twig": "^3.0"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/doctrine-fixtures-bundle": "^3.3",
|
||||
"drupol/php-conventions": "^5",
|
||||
@ -71,23 +68,17 @@
|
||||
"symfony/var-dumper": "^4.4",
|
||||
"symfony/web-profiler-bundle": "^4.4"
|
||||
},
|
||||
"config": {
|
||||
"bin-dir": "bin",
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true,
|
||||
"vendor-dir": "tests/app/vendor",
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"phpstan/extension-installer": true,
|
||||
"ergebnis/composer-normalize": true,
|
||||
"phpro/grumphp": true
|
||||
}
|
||||
"conflict": {
|
||||
"symfony/symfony": "*"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Chill\\ActivityBundle\\": "src/Bundle/ChillActivityBundle",
|
||||
"Chill\\AsideActivityBundle\\": "src/Bundle/ChillAsideActivityBundle/src",
|
||||
"Chill\\BudgetBundle\\": "src/Bundle/ChillBudgetBundle",
|
||||
"Chill\\CalendarBundle\\": "src/Bundle/ChillCalendarBundle",
|
||||
"Chill\\CustomFieldsBundle\\": "src/Bundle/ChillCustomFieldsBundle",
|
||||
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
|
||||
"Chill\\DocStoreBundle\\": "src/Bundle/ChillDocStoreBundle",
|
||||
"Chill\\EventBundle\\": "src/Bundle/ChillEventBundle",
|
||||
"Chill\\FamilyMemberBundle\\": "src/Bundle/ChillFamilyMemberBundle",
|
||||
@ -96,9 +87,6 @@
|
||||
"Chill\\ReportBundle\\": "src/Bundle/ChillReportBundle",
|
||||
"Chill\\TaskBundle\\": "src/Bundle/ChillTaskBundle",
|
||||
"Chill\\ThirdPartyBundle\\": "src/Bundle/ChillThirdPartyBundle",
|
||||
"Chill\\AsideActivityBundle\\": "src/Bundle/ChillAsideActivityBundle/src",
|
||||
"Chill\\DocGeneratorBundle\\": "src/Bundle/ChillDocGeneratorBundle",
|
||||
"Chill\\CalendarBundle\\": "src/Bundle/ChillCalendarBundle",
|
||||
"Chill\\WopiBundle\\": "src/Bundle/ChillWopiBundle/src"
|
||||
}
|
||||
},
|
||||
@ -111,10 +99,10 @@
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"composer/package-versions-deprecated": true,
|
||||
"phpstan/extension-installer": true,
|
||||
"ergebnis/composer-normalize": true,
|
||||
"ocramius/package-versions": true,
|
||||
"phpro/grumphp": true,
|
||||
"ocramius/package-versions": true
|
||||
"phpstan/extension-installer": true
|
||||
},
|
||||
"bin-dir": "bin",
|
||||
"optimize-autoloader": true,
|
||||
@ -123,8 +111,8 @@
|
||||
},
|
||||
"scripts": {
|
||||
"auto-scripts": {
|
||||
"cache:clear": "symfony-cmd",
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd"
|
||||
"assets:install %PUBLIC_DIR%": "symfony-cmd",
|
||||
"cache:clear": "symfony-cmd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +0,0 @@
|
||||
add npm/yarn dependency in package.json :
|
||||
|
||||
"select2-bootstrap-theme": "0.1.0-beta.10",
|
@ -31,6 +31,7 @@ use DateTime;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use InvalidArgumentException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
@ -38,8 +39,8 @@ use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
final class ActivityController extends AbstractController
|
||||
@ -471,20 +472,21 @@ final class ActivityController extends AbstractController
|
||||
|
||||
public function showAction(Request $request, int $id): Response
|
||||
{
|
||||
$view = null;
|
||||
$entity = $this->activityRepository->find($id);
|
||||
|
||||
[$person, $accompanyingPeriod] = $this->getEntity($request);
|
||||
if (null === $entity) {
|
||||
throw $this->createNotFoundException('Unable to find Activity entity.');
|
||||
}
|
||||
|
||||
$accompanyingPeriod = $entity->getAccompanyingPeriod();
|
||||
$person = $entity->getPerson();
|
||||
|
||||
if ($accompanyingPeriod instanceof AccompanyingPeriod) {
|
||||
$view = 'ChillActivityBundle:Activity:showAccompanyingCourse.html.twig';
|
||||
} elseif ($person instanceof Person) {
|
||||
$view = 'ChillActivityBundle:Activity:showPerson.html.twig';
|
||||
}
|
||||
|
||||
$entity = $this->activityRepository->find($id);
|
||||
|
||||
if (null === $entity) {
|
||||
throw $this->createNotFoundException('Unable to find Activity entity.');
|
||||
} else {
|
||||
throw new RuntimeException('the activity should be linked with a period or person');
|
||||
}
|
||||
|
||||
if (null !== $accompanyingPeriod) {
|
||||
@ -493,8 +495,7 @@ final class ActivityController extends AbstractController
|
||||
$entity->personsNotAssociated = $entity->getPersonsNotAssociated();
|
||||
}
|
||||
|
||||
// TODO revoir le Voter de Activity pour tenir compte qu'une activité peut appartenir a une période
|
||||
// $this->denyAccessUnlessGranted('CHILL_ACTIVITY_SEE', $entity);
|
||||
$this->denyAccessUnlessGranted(ActivityVoter::SEE, $entity);
|
||||
|
||||
$deleteForm = $this->createDeleteForm($entity->getId(), $person, $accompanyingPeriod);
|
||||
|
||||
|
@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Entity;
|
||||
|
||||
use Chill\ActivityBundle\Validator\Constraints as ActivityValidator;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\MainBundle\Entity\Center;
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\HasCenterInterface;
|
||||
@ -67,7 +67,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityType")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
* @SerializedName("activityType")
|
||||
* @ORM\JoinColumn(name="type_id")
|
||||
*/
|
||||
@ -75,16 +75,19 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\ActivityBundle\Entity\ActivityPresence")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private ?ActivityPresence $attendee = null;
|
||||
|
||||
/**
|
||||
* @ORM\Embedded(class="Chill\MainBundle\Entity\Embeddable\CommentEmbeddable", columnPrefix="comment_")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private CommentEmbeddable $comment;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private DateTime $date;
|
||||
|
||||
@ -100,6 +103,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private bool $emergency = false;
|
||||
|
||||
@ -107,13 +111,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
* @ORM\Id
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Location")
|
||||
* @groups({"read"})
|
||||
* @groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Location $location = null;
|
||||
|
||||
@ -124,42 +128,45 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\Person")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Collection $persons = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\ActivityBundle\Entity\ActivityReason")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private Collection $reasons;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Scope")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private ?Scope $scope = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", options={"default": ""})
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private string $sentReceived = '';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialAction")
|
||||
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialaction")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private Collection $socialActions;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\PersonBundle\Entity\SocialWork\SocialIssue")
|
||||
* @ORM\JoinTable(name="chill_activity_activity_chill_person_socialissue")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private Collection $socialIssues;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\ThirdPartyBundle\Entity\ThirdParty")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Collection $thirdParties = null;
|
||||
|
||||
@ -170,12 +177,13 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private User $user;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\User")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
*/
|
||||
private ?Collection $users = null;
|
||||
|
||||
@ -191,7 +199,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
$this->socialActions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function addDocument(Document $document): self
|
||||
public function addDocument(StoredObject $document): self
|
||||
{
|
||||
$this->documents[] = $document;
|
||||
|
||||
@ -302,6 +310,18 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
return $this->documents;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
public function getDurationMinute(): int
|
||||
{
|
||||
if (null === $this->durationTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) round(($this->durationTime->getTimestamp() + $this->durationTime->getOffset()) / 60.0, 0);
|
||||
}
|
||||
|
||||
public function getDurationTime(): ?DateTime
|
||||
{
|
||||
return $this->durationTime;
|
||||
@ -402,6 +422,18 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
return $this->travelTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
public function getTravelTimeMinute(): int
|
||||
{
|
||||
if (null === $this->travelTime) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int) round(($this->travelTime->getTimestamp() + $this->travelTime->getOffset()) / 60.0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
@ -425,7 +457,7 @@ class Activity implements AccompanyingPeriodLinkedWithSocialIssuesEntityInterfac
|
||||
return $this->getEmergency();
|
||||
}
|
||||
|
||||
public function removeDocument(Document $document): void
|
||||
public function removeDocument(StoredObject $document): void
|
||||
{
|
||||
$this->documents->removeElement($document);
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\ActivityBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
|
||||
/**
|
||||
* Class ActivityPresence.
|
||||
@ -31,11 +32,14 @@ class ActivityPresence
|
||||
* @ORM\Id
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
*/
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
* @Serializer\Groups({"docgen:read"})
|
||||
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
|
||||
*/
|
||||
private array $name = [];
|
||||
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\ActivityBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use Symfony\Component\Serializer\Annotation\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
@ -118,6 +119,7 @@ class ActivityType
|
||||
* @ORM\Id
|
||||
* @ORM\Column(name="id", type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
* @Groups({"docgen:read"})
|
||||
*/
|
||||
private ?int $id;
|
||||
|
||||
@ -133,7 +135,8 @@ class ActivityType
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
* @Groups({"read"})
|
||||
* @Groups({"read", "docgen:read"})
|
||||
* @Serializer\Context({"is-translatable": true}, groups={"docgen:read"})
|
||||
*/
|
||||
private array $name = [];
|
||||
|
||||
|
@ -143,9 +143,17 @@
|
||||
</div>
|
||||
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
<div class="item-col item-meta">
|
||||
{% 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 %}
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
{{ recordAction }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -8,7 +8,7 @@
|
||||
action: 'show', displayBadge: true,
|
||||
targetEntity: { name: type, id: entity.id },
|
||||
buttonText: entity|chill_entity_render_string,
|
||||
isDead: entity.deathdate is not null,
|
||||
isDead: entity.deathdate is defined and entity.deathdate is not null,
|
||||
parent: parent
|
||||
} %}
|
||||
{% endmacro %}
|
||||
|
@ -83,15 +83,15 @@
|
||||
{{ form_row(edit_form.comment) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if edit_form.documents is defined -%}
|
||||
{{ form_row(edit_form.documents) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if edit_form.attendee is defined -%}
|
||||
{{ form_row(edit_form.attendee) }}
|
||||
{% endif %}
|
||||
|
||||
{# TODO .. status #}
|
||||
{%- if edit_form.documents is defined -%}
|
||||
{{ form_row(edit_form.documents) }}
|
||||
{% endif %}
|
||||
|
||||
<div data-docgen-template-picker="data-docgen-template-picker" data-entity-class="Chill\ActivityBundle\Entity\Activity" data-entity-id="{{ entity.id }}"></div>
|
||||
|
||||
{% set person_id = null %}
|
||||
{% if entity.person %}
|
||||
|
@ -24,10 +24,12 @@
|
||||
window.activity = {{ activity_json|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_activity') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('vue_activity') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
@ -39,9 +39,11 @@
|
||||
window.activity = {{ activity_json|json_encode|raw }};
|
||||
</script>
|
||||
{{ encore_entry_script_tags('vue_activity') }}
|
||||
{{ encore_entry_script_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
{{ encore_entry_link_tags('mod_async_upload') }}
|
||||
{{ encore_entry_link_tags('vue_activity') }}
|
||||
{{ encore_entry_link_tags('mod_docgen_picktemplate') }}
|
||||
{% endblock %}
|
||||
|
@ -1,58 +1,60 @@
|
||||
{% macro recordAction(activity, context = null, person_id = null, accompanying_course_id = null) %}
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
<li>
|
||||
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||
'entityId': activity.id
|
||||
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
{% if is_granted('CHILL_ACTIVITY_SEE_DETAILS', activity) %}
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
<a class="btn btn-notify" href="{{ chill_path_add_return_path('chill_main_notification_create', {
|
||||
'entityClass': 'Chill\\ActivityBundle\\Entity\\Activity',
|
||||
'entityId': activity.id
|
||||
}) }}">{{ 'notification.Notify'|trans }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
{% if context == 'person' and activity.accompanyingPeriod is not empty %}
|
||||
{#
|
||||
Disable person_id in following links, for redirect to accompanyingCourse context
|
||||
#}
|
||||
{% set person_id = null %}
|
||||
{% set accompanying_course_id = activity.accompanyingPeriod.id %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
<a href="{{ chill_path_add_return_path('chill_activity_activity_list',{
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
class="btn btn-primary"
|
||||
title="{{ 'See activity in accompanying course context'|trans }}">
|
||||
<i class="fa fa-random fa-fw"></i>
|
||||
{{ 'Period number %number%'|trans({'%number%': accompanying_course_id}) }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_show', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-show"
|
||||
title="{{ 'Show'|trans }}"></a>
|
||||
</li>
|
||||
{% if no_action is not defined or no_action == false %}
|
||||
{% if is_granted('CHILL_ACTIVITY_UPDATE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_edit', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-update"
|
||||
title="{{ 'Edit'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if is_granted('CHILL_ACTIVITY_DELETE', activity) %}
|
||||
<li>
|
||||
<a href="{{ path('chill_activity_activity_delete', {'id': activity.id,
|
||||
'person_id': person_id,
|
||||
'accompanying_period_id': accompanying_course_id
|
||||
}) }}"
|
||||
class="btn btn-delete"
|
||||
title="{{ 'Delete'|trans }}"></a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
|
@ -86,7 +86,7 @@
|
||||
{% include 'ChillActivityBundle:Activity:concernedGroups.html.twig' with {
|
||||
'context': context,
|
||||
'render': 'bloc',
|
||||
'badge_person': 'true'
|
||||
'badge_person': true
|
||||
} %}
|
||||
|
||||
<h2 class="chill-blue">{{ 'Activity data'|trans }}</h2>
|
||||
|
@ -0,0 +1,220 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\ActivityBundle\Service\DocGenerator;
|
||||
|
||||
use Chill\ActivityBundle\Entity\Activity;
|
||||
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithAdminFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\DocGeneratorContextWithPublicFormInterface;
|
||||
use Chill\DocGeneratorBundle\Context\Exception\UnexpectedTypeException;
|
||||
use Chill\DocGeneratorBundle\Entity\DocGeneratorTemplate;
|
||||
use Chill\DocGeneratorBundle\Service\Context\BaseContextData;
|
||||
use Chill\DocStoreBundle\Entity\Document;
|
||||
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||
use Chill\DocStoreBundle\Repository\DocumentCategoryRepository;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Chill\PersonBundle\Entity\AccompanyingPeriod;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\PersonBundle\Templating\Entity\PersonRender;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class ActivityContext implements
|
||||
DocGeneratorContextWithAdminFormInterface,
|
||||
DocGeneratorContextWithPublicFormInterface
|
||||
{
|
||||
private BaseContextData $baseContextData;
|
||||
|
||||
private DocumentCategoryRepository $documentCategoryRepository;
|
||||
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private NormalizerInterface $normalizer;
|
||||
|
||||
private PersonRender $personRender;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
public function __construct(
|
||||
DocumentCategoryRepository $documentCategoryRepository,
|
||||
NormalizerInterface $normalizer,
|
||||
TranslatableStringHelperInterface $translatableStringHelper,
|
||||
EntityManagerInterface $em,
|
||||
PersonRender $personRender,
|
||||
TranslatorInterface $translator,
|
||||
BaseContextData $baseContextData
|
||||
) {
|
||||
$this->documentCategoryRepository = $documentCategoryRepository;
|
||||
$this->normalizer = $normalizer;
|
||||
$this->translatableStringHelper = $translatableStringHelper;
|
||||
$this->em = $em;
|
||||
$this->personRender = $personRender;
|
||||
$this->translator = $translator;
|
||||
$this->baseContextData = $baseContextData;
|
||||
}
|
||||
|
||||
public function adminFormReverseTransform(array $data): array
|
||||
{
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function adminFormTransform(array $data): array
|
||||
{
|
||||
return [
|
||||
'mainPerson' => $data['mainPerson'] ?? false,
|
||||
'mainPersonLabel' => $data['mainPersonLabel'] ?? $this->translator->trans('docgen.Main person'),
|
||||
'person1' => $data['person1'] ?? false,
|
||||
'person1Label' => $data['person1Label'] ?? $this->translator->trans('docgen.person 1'),
|
||||
'person2' => $data['person2'] ?? false,
|
||||
'person2Label' => $data['person2Label'] ?? $this->translator->trans('docgen.person 2'),
|
||||
];
|
||||
}
|
||||
|
||||
public function buildAdminForm(FormBuilderInterface $builder): void
|
||||
{
|
||||
$builder
|
||||
->add('mainPerson', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for main person',
|
||||
])
|
||||
->add('mainPersonLabel', TextType::class, [
|
||||
'label' => 'main person label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('person1', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for person 1',
|
||||
])
|
||||
->add('person1Label', TextType::class, [
|
||||
'label' => 'person 1 label',
|
||||
'required' => true,
|
||||
])
|
||||
->add('person2', CheckboxType::class, [
|
||||
'required' => false,
|
||||
'label' => 'docgen.Ask for person 2',
|
||||
])
|
||||
->add('person2Label', TextType::class, [
|
||||
'label' => 'person 2 label',
|
||||
'required' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $entity
|
||||
*/
|
||||
public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
|
||||
{
|
||||
$options = $template->getOptions();
|
||||
$persons = $entity->getPersons();
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $key) {
|
||||
if ($options[$key] ?? false) {
|
||||
$builder->add($key, EntityType::class, [
|
||||
'class' => Person::class,
|
||||
'choices' => $persons,
|
||||
'choice_label' => function (Person $p) {
|
||||
return $this->personRender->renderString($p, []);
|
||||
},
|
||||
'multiple' => false,
|
||||
'expanded' => true,
|
||||
'label' => $options[$key . 'Label'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
|
||||
{
|
||||
if (!$entity instanceof Activity) {
|
||||
throw new UnexpectedTypeException($entity, Activity::class);
|
||||
}
|
||||
$options = $template->getOptions();
|
||||
|
||||
$data = [];
|
||||
$data = array_merge($data, $this->baseContextData->getData());
|
||||
$data['activity'] = $this->normalizer->normalize($entity, 'docgen', ['docgen:expects' => Activity::class, 'groups' => 'docgen:read']);
|
||||
|
||||
$data['course'] = $this->normalizer->normalize($entity->getAccompanyingPeriod(), 'docgen', ['docgen:expects' => AccompanyingPeriod::class, 'groups' => 'docgen:read']);
|
||||
$data['person'] = $this->normalizer->normalize($entity->getPerson(), 'docgen', ['docgen:expects' => Person::class, 'groups' => 'docgen:read']);
|
||||
|
||||
foreach (['mainPerson', 'person1', 'person2'] as $k) {
|
||||
if ($options[$k]) {
|
||||
$data[$k] = $this->normalizer->normalize($contextGenerationData[$k], 'docgen', [
|
||||
'docgen:expects' => Person::class,
|
||||
'groups' => 'docgen:read',
|
||||
'docgen:person:with-household' => true,
|
||||
'docgen:person:with-relations' => true,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'docgen.A basic context for activity';
|
||||
}
|
||||
|
||||
public function getEntityClass(): string
|
||||
{
|
||||
return Activity::class;
|
||||
}
|
||||
|
||||
public function getFormData(DocGeneratorTemplate $template, $entity): array
|
||||
{
|
||||
return [
|
||||
'activity' => $entity,
|
||||
];
|
||||
}
|
||||
|
||||
public static function getKey(): string
|
||||
{
|
||||
return self::class;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'docgen.Activity basic';
|
||||
}
|
||||
|
||||
public function hasAdminForm(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
|
||||
{
|
||||
$options = $template->getOptions();
|
||||
|
||||
return $options['mainPerson'] || $options['person1'] || $options['person2'];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Activity $entity
|
||||
*/
|
||||
public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
|
||||
{
|
||||
$doc = new StoredObject();
|
||||
// TODO push document to remote
|
||||
|
||||
$this->em->persist($doc);
|
||||
|
||||
$entity->addDocument($doc);
|
||||
}
|
||||
}
|
@ -32,3 +32,8 @@ services:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Validator/Constraints/'
|
||||
|
||||
Chill\ActivityBundle\Service\DocGenerator\:
|
||||
autowire: true
|
||||
autoconfigure: true
|
||||
resource: '../Service/DocGenerator/'
|
||||
|
@ -228,3 +228,7 @@ See activity in accompanying course context: Voir l'activité dans le contexte d
|
||||
You get notified of an activity which does not exists any more: Cette notification ne correspond pas à une activité valide.
|
||||
you are not allowed to see it details: La notification fait référence à une activité à laquelle vous n'avez pas accès.
|
||||
This is the minimal activity data: Activité n°
|
||||
|
||||
docgen:
|
||||
Activity basic: Echange
|
||||
A basic context for activity: Contexte pour les échanges
|
||||
|
@ -17,7 +17,7 @@
|
||||
<select class="form-select" v-model="template">
|
||||
<option disabled selected value="">{{ $t('choose_a_template') }}</option>
|
||||
<template v-for="t in templates">
|
||||
<option v-bind:value="t.id">{{ t.name.fr }}</option>
|
||||
<option v-bind:value="t.id">{{ t.name.fr || 'Aucun nom défini' }}</option>
|
||||
</template>
|
||||
</select>
|
||||
<button v-if="canGenerate" class="btn btn-update btn-sm change-icon" type="button" @click="generateDocument"><i class="fa fa-fw fa-cog"></i></button>
|
||||
@ -48,6 +48,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
templates: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
// beforeMove execute "something" before
|
||||
@ -73,7 +74,11 @@ export default {
|
||||
return true;
|
||||
},
|
||||
getDescription() {
|
||||
return this.templates.find(t => t.id === this.template).description || '';
|
||||
let desc = this.templates.find(t => t.id === this.template);
|
||||
if (null === desc) {
|
||||
return '';
|
||||
}
|
||||
return desc.description || '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -0,0 +1,52 @@
|
||||
{% import "@ChillDocStore/Macro/macro.html.twig" as m %}
|
||||
|
||||
<div class="flex-table accompanying_course_work-list">
|
||||
<div class="item-bloc document-item bg-chill-llight-gray">
|
||||
<div class="row justify-content-center my-4">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-4x fa-file-text-o text-success"></i>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h3>{{ document.title }}</h3>
|
||||
<small>{{ document.object.type }}</small>
|
||||
|
||||
{% if document.description is not empty %}
|
||||
<blockquote class="chill-user-quote mt-2">
|
||||
{{ document.description }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if display_action is defined and display_action == true %}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
<li>
|
||||
|
||||
{#
|
||||
data-button is optional !
|
||||
OPTIONS:
|
||||
'changeIcon' string
|
||||
'changeClass' string
|
||||
'noText' boolean
|
||||
|
||||
#}{% set button = {
|
||||
'changeIcon': 'fa-unlock',
|
||||
} %}
|
||||
|
||||
{# vue component #}
|
||||
<span
|
||||
data-module="wopi-link"
|
||||
data-wopi-url="{{ path('chill_wopi_file_edit', {'fileId': document.object.uuid}) }}"
|
||||
data-doc-title="{{ document.title|e('html_attr') }}"
|
||||
data-doc-type="{{ document.object.type|e('html_attr') }}"
|
||||
data-button="{{ button|json_encode }}"
|
||||
></span>
|
||||
</li>
|
||||
</ul>
|
||||
{% endif %}
|
@ -0,0 +1,19 @@
|
||||
{% import '@ChillMain/Workflow/macro_breadcrumb.html.twig' as m %}
|
||||
|
||||
<div class="flex-grow-1 {% if add_classes is defined %}{{ add_classes }}{% else %}h2{% endif %}">
|
||||
<div>
|
||||
{% if concerne is defined and concerne == true %}
|
||||
<span class="item-key">{{ 'Concerne'|trans }}: </span>
|
||||
{% endif %}
|
||||
|
||||
{{ 'workflow.Document (n°%doc%)'|trans({'%doc%': document.id}) }}
|
||||
|
||||
{% if description is defined and description == true %}
|
||||
{{ ' — ' ~ document.title }}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if breadcrumb is defined and breadcrumb == true %}
|
||||
{{ m.breadcrumb(_context) }}
|
||||
{% endif %}
|
||||
</div>
|
@ -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 %}
|
||||
|
||||
<h1>{{ 'Document %title%' | trans({ '%title%': document.title }) }}</h1>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ document.title }}</dd>
|
||||
|
||||
<div class="document-show">
|
||||
<h1>{{ block('title') }}</h1>
|
||||
|
||||
<dl class="chill_view_data">
|
||||
<dt>{{ 'Title'|trans }}</dt>
|
||||
<dd>{{ document.title }}</dd>
|
||||
|
||||
{% if document.category is not null %}
|
||||
<dt>{{ 'Category'|trans }}</dt>
|
||||
<dd>{{ document.category.name|localize_translatable_string }}</dd>
|
||||
{% endif %}
|
||||
|
||||
<dt>{{ 'Description' | trans }}</dt>
|
||||
<dd>
|
||||
{% if document.description is empty %}
|
||||
<span class="chill-no-data-statement">{{ 'Any description'|trans }}</span>
|
||||
{% else %}
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ document.description|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'id': document.id, 'course': accompanyingCourse.id}) }}" class="btn btn-edit">
|
||||
{{ 'Edit' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block block_post_menu %}
|
||||
<div class="post-menu pt-4">
|
||||
{% set workflows_frame = chill_entity_workflow_list('Chill\\DocStoreBundle\\Entity\\AccompanyingCourseDocument', document.id) %}
|
||||
{% if workflows_frame is not empty %}
|
||||
{{ workflows_frame|raw }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
<dt>{{ 'Description' | trans }}</dt>
|
||||
<dd>
|
||||
{% if document.description is empty %}
|
||||
<span class="chill-no-data-statement">{{ 'Any description'|trans }}</span>
|
||||
{% else %}
|
||||
<blockquote class="chill-user-quote">
|
||||
{{ document.description|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
</dl>
|
||||
|
||||
<ul class="record_actions">
|
||||
<li class="cancel">
|
||||
<a href="{{ path('accompanying_course_document_index', {'course': accompanyingCourse.id}) }}" class="btn btn-cancel">
|
||||
{{ 'Back to the list' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
{{ m.download_button(document.object, document.title) }}
|
||||
</li>
|
||||
|
||||
{% if is_granted('CHILL_ACCOMPANYING_COURSE_DOCUMENT_UPDATE', document) %}
|
||||
<li>
|
||||
<a href="{{ path('accompanying_course_document_edit', {'id': document.id, 'course': accompanyingCourse.id}) }}" class="btn btn-edit">
|
||||
{{ 'Edit' | trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% block js %}
|
||||
{{ parent() }}
|
||||
{{ encore_entry_script_tags('mod_async_upload') }}
|
||||
{{ encore_entry_script_tags('mod_entity_workflow_pick') }}
|
||||
{% endblock %}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\DocStoreBundle\Workflow;
|
||||
|
||||
use Chill\DocStoreBundle\Entity\AccompanyingCourseDocument;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowHandlerInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
|
||||
class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandlerInterface
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
/**
|
||||
* TODO: injecter le repository directement.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -27,3 +27,8 @@ services:
|
||||
autoconfigure: true
|
||||
tags:
|
||||
- { name: chill.role }
|
||||
|
||||
Chill\DocStoreBundle\Workflow\:
|
||||
resource: './../Workflow/'
|
||||
autoconfigure: true
|
||||
autowire: true
|
||||
|
@ -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
|
||||
|
@ -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());
|
||||
|
118
src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php
Normal file
118
src/Bundle/ChillMainBundle/Controller/WorkflowApiController.php
Normal file
@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use LogicException;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
|
||||
class WorkflowApiController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(Security $security, EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->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
|
||||
);
|
||||
}
|
||||
}
|
257
src/Bundle/ChillMainBundle/Controller/WorkflowController.php
Normal file
257
src/Bundle/ChillMainBundle/Controller/WorkflowController.php
Normal file
@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowComment;
|
||||
use Chill\MainBundle\Form\EntityWorkflowCommentType;
|
||||
use Chill\MainBundle\Form\WorkflowStepType;
|
||||
use Chill\MainBundle\Pagination\PaginatorFactory;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\TransitionBlocker;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
use function count;
|
||||
|
||||
class WorkflowController extends AbstractController
|
||||
{
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private EntityWorkflowRepository $entityWorkflowRepository;
|
||||
|
||||
private PaginatorFactory $paginatorFactory;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
private TranslatorInterface $translator;
|
||||
|
||||
private ValidatorInterface $validator;
|
||||
|
||||
public function __construct(EntityWorkflowManager $entityWorkflowManager, EntityWorkflowRepository $entityWorkflowRepository, ValidatorInterface $validator, PaginatorFactory $paginatorFactory, Registry $registry, EntityManagerInterface $entityManager, TranslatorInterface $translator)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -168,6 +168,7 @@ class ChillMainExtension extends Extension implements
|
||||
$loader->load('services/timeline.yaml');
|
||||
$loader->load('services/search.yaml');
|
||||
$loader->load('services/serializer.yaml');
|
||||
$loader->load('services/mailer.yaml');
|
||||
|
||||
$this->configureCruds($container, $config['cruds'], $config['apis'], $loader);
|
||||
}
|
||||
@ -391,6 +392,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,
|
||||
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Model;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait TrackCreationTrait
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
|
||||
*/
|
||||
private ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $createdBy = null;
|
||||
|
||||
public function getCreatedAt(): ?DateTimeInterface
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function getCreatedBy(): ?User
|
||||
{
|
||||
return $this->createdBy;
|
||||
}
|
||||
|
||||
public function setCreatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->createdAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setCreatedBy(User $user): self
|
||||
{
|
||||
$this->createdBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Model;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
trait TrackUpdateTrait
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable", nullable=true, options={"default": NULL})
|
||||
*/
|
||||
private ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $updatedBy = null;
|
||||
|
||||
public function getUpdatedAt(): ?DateTimeInterface
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function getUpdatedBy(): ?User
|
||||
{
|
||||
return $this->updatedBy;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(DateTimeInterface $datetime): self
|
||||
{
|
||||
$this->updatedAt = $datetime instanceof DateTime ? DateTimeImmutable::createFromMutable($datetime) : $datetime;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setUpdatedBy(User $user): self
|
||||
{
|
||||
$this->updatedBy = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -64,7 +64,7 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255, nullable=true)
|
||||
* @Serializer\Groups({"read", "write"})
|
||||
* @Serializer\Groups({"read", "write", "docgen:read"})
|
||||
*/
|
||||
private ?string $email = null;
|
||||
|
||||
|
@ -23,6 +23,9 @@ use Symfony\Component\Validator\Constraints as Assert;
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(
|
||||
* name="chill_main_notification",
|
||||
* indexes={
|
||||
* @ORM\Index(name="chill_main_notification_related_entity_idx", columns={"relatedentityclass", "relatedentityid"})
|
||||
* }
|
||||
* )
|
||||
* @ORM\HasLifecycleCallbacks
|
||||
*/
|
||||
|
439
src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
Normal file
439
src/Bundle/ChillMainBundle/Entity/Workflow/EntityWorkflow.php
Normal file
@ -0,0 +1,439 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
||||
use DateTimeInterface;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Iterator;
|
||||
use RuntimeException;
|
||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity")
|
||||
* @EntityWorkflowCreation(groups={"creation"})
|
||||
* @Serializer\DiscriminatorMap(typeProperty="type", mapping={
|
||||
* "entity_workflow": EntityWorkflow::class
|
||||
* })
|
||||
*/
|
||||
class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=EntityWorkflowComment::class, mappedBy="entityWorkflow", orphanRemoval=true)
|
||||
*
|
||||
* @var Collection|EntityWorkflowComment[]
|
||||
*/
|
||||
private Collection $comments;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
* @Serializer\Groups({"read"})
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", length=255)
|
||||
*/
|
||||
private string $relatedEntityClass = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $relatedEntityId;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=EntityWorkflowStep::class, mappedBy="entityWorkflow", orphanRemoval=true, cascade={"persist"})
|
||||
* @ORM\OrderBy({"transitionAt": "ASC", "id": "ASC"})
|
||||
*
|
||||
* @var Collection|EntityWorkflowStep[]
|
||||
*/
|
||||
private Collection $steps;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_final")
|
||||
*
|
||||
* @var Collection|User[]
|
||||
*/
|
||||
private Collection $subscriberToFinal;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_subscriber_to_step")
|
||||
*
|
||||
* @var Collection|User[]
|
||||
*/
|
||||
private Collection $subscriberToStep;
|
||||
|
||||
/**
|
||||
* a step which will store all the transition data.
|
||||
*/
|
||||
private ?EntityWorkflowStep $transitionningStep = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private string $workflowName;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackCreationTrait;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity_comment")
|
||||
*/
|
||||
class EntityWorkflowComment implements TrackCreationInterface, TrackUpdateInterface
|
||||
{
|
||||
use TrackCreationTrait;
|
||||
use TrackUpdateTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""})
|
||||
*/
|
||||
private string $comment = '';
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="comments")
|
||||
*/
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
public function getComment(): string
|
||||
{
|
||||
return $this->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;
|
||||
}
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||
use function count;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table("chill_main_workflow_entity_step")
|
||||
*/
|
||||
class EntityWorkflowStep
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="text", options={"default": ""})
|
||||
*/
|
||||
private string $comment = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text")
|
||||
*/
|
||||
private ?string $currentStep = '';
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="json")
|
||||
*/
|
||||
private array $destEmail = [];
|
||||
|
||||
/**
|
||||
* @ORM\ManyToMany(targetEntity=User::class)
|
||||
* @ORM\JoinTable(name="chill_main_workflow_entity_step_user")
|
||||
*/
|
||||
private Collection $destUser;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=EntityWorkflow::class, inversedBy="steps")
|
||||
*/
|
||||
private ?EntityWorkflow $entityWorkflow = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
*/
|
||||
private bool $finalizeAfter = false;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="boolean", options={"default": false})
|
||||
*/
|
||||
private bool $freezeAfter = false;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private ?int $id = null;
|
||||
|
||||
/**
|
||||
* filled by @see{EntityWorkflow::getStepsChained}.
|
||||
*/
|
||||
private ?EntityWorkflowStep $next = null;
|
||||
|
||||
/**
|
||||
* filled by @see{EntityWorkflow::getStepsChained}.
|
||||
*/
|
||||
private ?EntityWorkflowStep $previous = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true, options={"default": null})
|
||||
*/
|
||||
private ?string $transitionAfter = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="datetime_immutable")
|
||||
*/
|
||||
private ?DateTimeImmutable $transitionAt = null;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=User::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*/
|
||||
private ?User $transitionBy = null;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="text", nullable=true)
|
||||
*/
|
||||
private ?string $transitionByEmail = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class EntityWorkflowCommentType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->add('comment', ChillTextareaType::class, [
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
@ -12,14 +12,18 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form\Type\DataTransformer;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\PersonBundle\Entity\Person;
|
||||
use Chill\ThirdPartyBundle\Entity\ThirdParty;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use UnexpectedValueException;
|
||||
|
||||
use function array_key_exists;
|
||||
|
||||
class UserToJsonTransformer implements DataTransformerInterface
|
||||
class EntityToJsonTransformer implements DataTransformerInterface
|
||||
{
|
||||
private DenormalizerInterface $denormalizer;
|
||||
|
||||
@ -27,23 +31,36 @@ class UserToJsonTransformer implements DataTransformerInterface
|
||||
|
||||
private SerializerInterface $serializer;
|
||||
|
||||
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple)
|
||||
private string $type;
|
||||
|
||||
public function __construct(DenormalizerInterface $denormalizer, SerializerInterface $serializer, bool $multiple, string $type)
|
||||
{
|
||||
$this->denormalizer = $denormalizer;
|
||||
$this->serializer = $serializer;
|
||||
$this->multiple = $multiple;
|
||||
$this->type = $type;
|
||||
}
|
||||
|
||||
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));
|
||||
if ('' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->denormalizeOne($denormalized);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -60,7 +77,7 @@ class UserToJsonTransformer implements DataTransformerInterface
|
||||
]);
|
||||
}
|
||||
|
||||
private function denormalizeOne(array $item): User
|
||||
private function denormalizeOne(array $item)
|
||||
{
|
||||
if (!array_key_exists('type', $item)) {
|
||||
throw new TransformationFailedException('the key "type" is missing on element');
|
||||
@ -70,10 +87,30 @@ class UserToJsonTransformer implements DataTransformerInterface
|
||||
throw new TransformationFailedException('the key "id" is missing on element');
|
||||
}
|
||||
|
||||
switch ($this->type) {
|
||||
case 'user':
|
||||
$class = User::class;
|
||||
|
||||
break;
|
||||
|
||||
case 'person':
|
||||
$class = Person::class;
|
||||
|
||||
break;
|
||||
|
||||
case 'thirdparty':
|
||||
$class = ThirdParty::class;
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new UnexpectedValueException('This type is not supported');
|
||||
}
|
||||
|
||||
return
|
||||
$this->denormalizer->denormalize(
|
||||
['type' => $item['type'], 'id' => $item['id']],
|
||||
User::class,
|
||||
$class,
|
||||
'json',
|
||||
[AbstractNormalizer::GROUPS => ['read']],
|
||||
);
|
@ -12,7 +12,7 @@ declare(strict_types=1);
|
||||
namespace Chill\MainBundle\Form\Type;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\UserToJsonTransformer;
|
||||
use Chill\MainBundle\Form\Type\DataTransformer\EntityToJsonTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
@ -38,7 +38,7 @@ class PickUserDynamicType extends AbstractType
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->addViewTransformer(new UserToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple']));
|
||||
$builder->addViewTransformer(new EntityToJsonTransformer($this->denormalizer, $this->serializer, $options['multiple'], 'user'));
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
@ -58,6 +58,6 @@ class PickUserDynamicType extends AbstractType
|
||||
|
||||
public function getBlockPrefix()
|
||||
{
|
||||
return 'pick_user_dynamic';
|
||||
return 'pick_entity_dynamic';
|
||||
}
|
||||
}
|
||||
|
111
src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
Normal file
111
src/Bundle/ChillMainBundle/Form/WorkflowStepType.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
use LogicException;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\Transition;
|
||||
|
||||
class WorkflowStepType extends AbstractType
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
public function __construct(EntityWorkflowManager $entityWorkflowManager, Registry $registry)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
36
src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php
Normal file
36
src/Bundle/ChillMainBundle/Form/WorkflowTransitionType.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Form;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class WorkflowTransitionType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
->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);
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ class NotificationMailer
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
->to($dest->getEmail())
|
||||
->subject('Re: [Chill] ' . $comment->getNotification()->getTitle())
|
||||
->subject('Re: ' . $comment->getNotification()->getTitle())
|
||||
->textTemplate('@ChillMain/Notification/email_notification_comment_persist.fr.md.twig')
|
||||
->context([
|
||||
'comment' => $comment,
|
||||
@ -79,11 +79,13 @@ class NotificationMailer
|
||||
continue;
|
||||
}
|
||||
|
||||
$email = new Email();
|
||||
$email
|
||||
->subject($notification->getTitle());
|
||||
|
||||
if ($notification->isSystem()) {
|
||||
$email = new Email();
|
||||
$email
|
||||
->text($notification->getMessage())
|
||||
->subject('[Chill] ' . $notification->getTitle());
|
||||
->text($notification->getMessage());
|
||||
} else {
|
||||
$email = new TemplatedEmail();
|
||||
$email
|
||||
|
@ -15,12 +15,15 @@ use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\NotificationRepository;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use function array_key_exists;
|
||||
|
||||
/**
|
||||
* Helps to find if a notification exist for a given entity.
|
||||
*/
|
||||
class NotificationPresence
|
||||
{
|
||||
private array $cache = [];
|
||||
|
||||
private NotificationRepository $notificationRepository;
|
||||
|
||||
private Security $security;
|
||||
@ -31,6 +34,29 @@ class NotificationPresence
|
||||
$this->notificationRepository = $notificationRepository;
|
||||
}
|
||||
|
||||
public function countNotificationsForClassAndEntity(string $relatedEntityClass, int $relatedEntityId): array
|
||||
{
|
||||
if (array_key_exists($relatedEntityClass, $this->cache) && array_key_exists($relatedEntityId, $this->cache[$relatedEntityClass])) {
|
||||
return $this->cache[$relatedEntityClass][$relatedEntityId];
|
||||
}
|
||||
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
$counter = $this->notificationRepository->countNotificationByRelatedEntityAndUserAssociated(
|
||||
$relatedEntityClass,
|
||||
$relatedEntityId,
|
||||
$user
|
||||
);
|
||||
|
||||
$this->cache[$relatedEntityClass][$relatedEntityId] = $counter;
|
||||
|
||||
return $counter;
|
||||
}
|
||||
|
||||
return ['unread' => 0, 'read' => 0];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
|
@ -23,6 +23,13 @@ class NotificationTwigExtension extends AbstractExtension
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
new TwigFunction('chill_count_notifications', [NotificationTwigExtensionRuntime::class, 'countNotificationsFor'], [
|
||||
'is_safe' => [],
|
||||
]),
|
||||
new TwigFunction('chill_counter_notifications', [NotificationTwigExtensionRuntime::class, 'counterNotificationFor'], [
|
||||
'needs_environment' => true,
|
||||
'is_safe' => ['html'],
|
||||
]),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,21 @@ class NotificationTwigExtensionRuntime implements RuntimeExtensionInterface
|
||||
$this->notificationPresence = $notificationPresence;
|
||||
}
|
||||
|
||||
public function counterNotificationFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
return $environment->render(
|
||||
'@ChillMain/Notification/extension_counter_notifications_for.html.twig',
|
||||
[
|
||||
'counter' => $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function countNotificationsFor(string $relatedEntityClass, int $relatedEntityId, array $options = []): array
|
||||
{
|
||||
return $this->notificationPresence->countNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
|
||||
}
|
||||
|
||||
public function listNotificationsFor(Environment $environment, string $relatedEntityClass, int $relatedEntityId, array $options = []): string
|
||||
{
|
||||
$notifications = $this->notificationPresence->getNotificationsForClassAndEntity($relatedEntityClass, $relatedEntityId);
|
||||
|
@ -13,6 +13,7 @@ namespace Chill\MainBundle\Repository;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\DBAL\Statement;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
@ -24,6 +25,8 @@ final class NotificationRepository implements ObjectRepository
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private ?Statement $notificationByRelatedEntityAndUserAssociatedStatement = null;
|
||||
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
@ -48,6 +51,30 @@ final class NotificationRepository implements ObjectRepository
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
public function countNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
|
||||
{
|
||||
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
|
||||
$sql =
|
||||
'SELECT
|
||||
SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread,
|
||||
SUM((cmn.sender_id = 1812)::int) AS sent,
|
||||
COUNT(cmn.*) AS total
|
||||
FROM chill_main_notification cmn
|
||||
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
|
||||
$this->notificationByRelatedEntityAndUserAssociatedStatement =
|
||||
$this->em->getConnection()->prepare($sql);
|
||||
}
|
||||
|
||||
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
|
||||
->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]);
|
||||
|
||||
$result = $results->fetchAssociative();
|
||||
|
||||
$results->free();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function countUnreadByUser(User $user): int
|
||||
{
|
||||
$sql = 'SELECT count(*) AS c FROM chill_main_notification_addresses_unread WHERE user_id = :userId';
|
||||
@ -153,11 +180,29 @@ final class NotificationRepository implements ObjectRepository
|
||||
* @return array|Notification[]
|
||||
*/
|
||||
public function findNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): array
|
||||
{
|
||||
return
|
||||
$this->buildQueryNotificationByRelatedEntityAndUserAssociated($relatedEntityClass, $relatedEntityId, $user)
|
||||
->select('n')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Notification::class;
|
||||
}
|
||||
|
||||
private function buildQueryNotificationByRelatedEntityAndUserAssociated(string $relatedEntityClass, int $relatedEntityId, User $user): QueryBuilder
|
||||
{
|
||||
$qb = $this->repository->createQueryBuilder('n');
|
||||
|
||||
$qb
|
||||
->select('n')
|
||||
->where($qb->expr()->eq('n.relatedEntityClass', ':relatedEntityClass'))
|
||||
->andWhere($qb->expr()->eq('n.relatedEntityId', ':relatedEntityId'))
|
||||
->andWhere($qb->expr()->isNotNull('n.sender'))
|
||||
@ -171,17 +216,7 @@ final class NotificationRepository implements ObjectRepository
|
||||
->setParameter('relatedEntityId', $relatedEntityId)
|
||||
->setParameter('user', $user);
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
public function findOneBy(array $criteria, ?array $orderBy = null): ?Notification
|
||||
{
|
||||
return $this->repository->findOneBy($criteria, $orderBy);
|
||||
}
|
||||
|
||||
public function getClassName()
|
||||
{
|
||||
return Notification::class;
|
||||
return $qb;
|
||||
}
|
||||
|
||||
private function queryByAddressee(User $addressee, bool $countQuery = false): QueryBuilder
|
||||
|
@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Repository\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ObjectRepository;
|
||||
|
||||
class EntityWorkflowRepository implements ObjectRepository
|
||||
{
|
||||
private EntityRepository $repository;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -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: '/ ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
@ -39,6 +39,7 @@ var ShowHide = function(options) {
|
||||
contents.push(el);
|
||||
}
|
||||
container_content.push(contents);
|
||||
// console.log('container content', container_content);
|
||||
}
|
||||
|
||||
// attach the listener on each input
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
@ -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:
|
||||
'<pick-workflow-vue ' +
|
||||
':relatedEntityClass="relatedEntityClass" ' +
|
||||
':relatedEntityId="relatedEntityId" ' +
|
||||
':workflowsAvailables="workflowsAvailables" ' +
|
||||
'></pick-workflow-vue>',
|
||||
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:
|
||||
'<list-workflow-vue ' +
|
||||
':workflows="workflows" ' +
|
||||
'></list-workflow-vue>',
|
||||
data() {
|
||||
return {
|
||||
workflows: JSON.parse(el.dataset.workflows),
|
||||
}
|
||||
}
|
||||
};
|
||||
createApp(app).mount(el);
|
||||
})
|
||||
;
|
@ -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: '<entity-workflow-vue-subscriber :entityWorkflowId="this.entityWorkflowId" :subscriberStep="this.subscriberStep" :subscriberFinal="this.subscriberFinal" @subscriptionUpdated="onUpdate"></entity-workflow-vue-subscriber>',
|
||||
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);
|
||||
})
|
@ -5,18 +5,21 @@ import { appMessages } from 'ChillMainAssets/vuejs/PickEntity/i18n';
|
||||
|
||||
const i18n = _createI18n(appMessages);
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function(e) {
|
||||
let appsOnPage = new Map();
|
||||
|
||||
let apps = document.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
function loadDynamicPicker(element) {
|
||||
|
||||
let apps = element.querySelectorAll('[data-module="pick-dynamic"]');
|
||||
|
||||
apps.forEach(function(el) {
|
||||
|
||||
const
|
||||
isMultiple = parseInt(el.dataset.multiple) === 1,
|
||||
input = document.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
|
||||
picked = isMultiple ? JSON.parse(input.value) : [JSON.parse(input.value)];
|
||||
uniqId = el.dataset.uniqid,
|
||||
input = element.querySelector('[data-input-uniqid="'+ el.dataset.uniqid +'"]'),
|
||||
picked = (isMultiple) ? (JSON.parse(input.value)) : ((input.value === '[]') ? (null) : ([JSON.parse(input.value)]));
|
||||
|
||||
createApp({
|
||||
const app = createApp({
|
||||
template: '<pick-entity ' +
|
||||
':multiple="multiple" ' +
|
||||
':types="types" ' +
|
||||
@ -31,7 +34,7 @@ window.addEventListener('DOMContentLoaded', function(e) {
|
||||
return {
|
||||
multiple: isMultiple,
|
||||
types: JSON.parse(el.dataset.types),
|
||||
picked,
|
||||
picked: picked === null ? [] : picked,
|
||||
uniqid: el.dataset.uniqid,
|
||||
}
|
||||
},
|
||||
@ -65,5 +68,36 @@ window.addEventListener('DOMContentLoaded', function(e) {
|
||||
})
|
||||
.use(i18n)
|
||||
.mount(el);
|
||||
|
||||
appsOnPage.set(uniqId, app);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
document.addEventListener('show-hide-show', function(e) {
|
||||
console.log('creation event caught')
|
||||
loadDynamicPicker(e.detail.container)
|
||||
})
|
||||
|
||||
document.addEventListener('show-hide-hide', function(e) {
|
||||
console.log('hiding event caught')
|
||||
e.detail.container.querySelectorAll('[data-module="pick-dynamic"]').forEach((el) => {
|
||||
let uniqId = el.dataset.uniqid;
|
||||
console.log(uniqId);
|
||||
if (appsOnPage.has(uniqId)) {
|
||||
appsOnPage.get(uniqId).unmount();
|
||||
console.log('App has been unmounted')
|
||||
appsOnPage.delete(uniqId);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function(e) {
|
||||
console.log('loaded event', e)
|
||||
loadDynamicPicker(document)
|
||||
})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -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: '<open-wopi-link :wopiUrl="wopiUrl" :title="title" :type="type" :button="button"></open-wopi-link>',
|
||||
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)
|
||||
;
|
||||
})
|
||||
;
|
||||
});
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
})
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<ul class="list-suggest remove-items">
|
||||
<ul class="list-suggest remove-items" v-if="picked.length">
|
||||
<li v-for="p in picked" @click="removeEntity(p)" :key="p.type+p.id">
|
||||
<span class="chill_denomination">{{ p.text }}</span>
|
||||
</li>
|
||||
@ -79,11 +79,15 @@ export default {
|
||||
);
|
||||
this.$refs.addPersons.resetSearch(); // to cast child method
|
||||
modal.showModal = false;
|
||||
console.log(this.picked)
|
||||
},
|
||||
removeEntity(entity) {
|
||||
console.log('remove entity', entity);
|
||||
this.$emit('removeEntity', entity);
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(this.picked);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<div class="d-grid gap-2 my-3">
|
||||
<button class="btn btn-misc" type="button" v-if="!subscriberFinal" @click="subscribeTo('subscribe', 'final')">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_final') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="subscriberFinal" @click="subscribeTo('unsubscribe', 'final')">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
{{ $t('unsubscribe_final') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="!subscriberStep" @click="subscribeTo('subscribe', 'step')">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</button>
|
||||
<button class="btn btn-misc" type="button" v-if="subscriberStep" @click="subscribeTo('unsubscribe', 'step')">
|
||||
<i class="fa fa-times fa-fw"></i>
|
||||
{{ $t('unsubscribe_all_steps') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {makeFetch} from 'ChillMainAssets/lib/api/apiMethods.js';
|
||||
|
||||
export default {
|
||||
name: "EntityWorkflowVueSubscriber",
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
subscribe_final: "Recevoir une notification à l'étape finale",
|
||||
unsubscribe_final: "Ne plus recevoir de notification à l'étape finale",
|
||||
subscribe_all_steps: "Recevoir une notification à chaque étape du suivi",
|
||||
unsubscribe_all_steps: "Ne plus recevoir de notification à chaque étape du suivi",
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
entityWorkflowId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
subscriberStep: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
subscriberFinal: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ['subscriptionUpdated'],
|
||||
methods: {
|
||||
subscribeTo(step, to) {
|
||||
let params = new URLSearchParams();
|
||||
params.set('subscribe', to);
|
||||
|
||||
const url = `/api/1.0/main/workflow/${this.entityWorkflowId}/${step}?` + params.toString();
|
||||
|
||||
makeFetch('POST', url).then(response => {
|
||||
this.$emit('subscriptionUpdated', response);
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
/*
|
||||
* ALTERNATIVES
|
||||
*
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="laststep">
|
||||
<label class="form-check-label" for="laststep">{{ $t('subscribe_final') }}</label>
|
||||
</div>
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="allsteps">
|
||||
<label class="form-check-label" for="allsteps">{{ $t('subscribe_all_steps') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="list-group my-3">
|
||||
<label class="list-group-item">
|
||||
<input class="form-check-input me-1" type="checkbox" value="">
|
||||
{{ $t('subscribe_final') }}
|
||||
</label>
|
||||
<label class="list-group-item">
|
||||
<input class="form-check-input me-1" type="checkbox" value="">
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-vertical my-3" role="group">
|
||||
<button type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_final') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary">
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ $t('subscribe_all_steps') }}
|
||||
</button>
|
||||
</div>
|
||||
*/
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div class="list-group my-2 workflow workflow-box">
|
||||
<div class="list-group-item">
|
||||
<h4>Workflow associés</h4>
|
||||
</div>
|
||||
<div class="list-group-item" v-for="w in workflows">
|
||||
{{ w.id }}
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a class="btn btn-sm btn-outline-primary"
|
||||
title="voir"
|
||||
:href="goToUrl(w)">
|
||||
<i class="fa fa-eye fa-fw"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ListWorkflow",
|
||||
props: {
|
||||
workflows: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
goToUrl(w) {
|
||||
return `/fr/main/workflow/${w.id}/show`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<template v-if="workflowsAvailables.length >= 1">
|
||||
<div class="dropdown d-grid gap-2">
|
||||
<button class="btn btn-primary dropdown-toggle" type="button" id="createWorkflowButton" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
Créer un workflow
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labelledby="createWorkflowButton">
|
||||
<li v-for="w in workflowsAvailables" :key="w.name">
|
||||
<a class="dropdown-item" :href="makeLink(w.name)" @click="goToGenerateWorkflow($event, w.name)">{{ w.text }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import {buildLinkCreate} from 'ChillMainAssets/lib/entity-workflow/api.js';
|
||||
|
||||
export default {
|
||||
name: "PickWorkflow",
|
||||
props: {
|
||||
relatedEntityClass: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
relatedEntityId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
workflowsAvailables: {
|
||||
type: Array,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
emits: ['goToGenerateWorkflow'],
|
||||
methods: {
|
||||
makeLink(workflowName) {
|
||||
return buildLinkCreate(workflowName, this.relatedEntityClass, this.relatedEntityId);
|
||||
},
|
||||
goToGenerateWorkflow(event, workflowName) {
|
||||
this.$emit('goToGenerateWorkflow', {event, workflowName, link: this.makeLink(workflowName)});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
@ -15,7 +15,7 @@
|
||||
<div class="modal-body">
|
||||
<slot name="body"></slot>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer" v-if="!hideFooter">
|
||||
<button class="btn btn-cancel" @click="$emit('close')">{{ $t('action.close') }}</button>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
@ -38,7 +38,16 @@
|
||||
*/
|
||||
export default {
|
||||
name: 'Modal',
|
||||
props: ['modalDialogClass'],
|
||||
props: {
|
||||
modalDialogClass: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
hideFooter: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
emits: ['close']
|
||||
}
|
||||
</script>
|
||||
|
@ -0,0 +1,243 @@
|
||||
<template>
|
||||
<a v-if="isOpenDocument"
|
||||
class="btn change-icon" :class="[isChangeClass ? button.changeClass : 'btn-edit']"
|
||||
@click="openModal">
|
||||
|
||||
<i class="fa me-2" :class="[isChangeIcon ? button.changeIcon : 'fa-pencil']"></i>
|
||||
|
||||
<span v-if="!noText">
|
||||
{{ $t('Update_document') }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<teleport to="body">
|
||||
<div class="wopi-frame" v-if="isOpenDocument">
|
||||
<modal v-if="modal.showModal"
|
||||
:modalDialogClass="modal.modalDialogClass"
|
||||
:hideFooter=true
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<img class="logo" :src="logo" height="45"/>
|
||||
<span class="ms-auto me-3">
|
||||
{{ this.title }}
|
||||
</span>
|
||||
<a class="btn btn-outline-light">
|
||||
<i class="fa fa-save fa-fw"></i>
|
||||
{{ $t('save_and_quit') }}
|
||||
</a>
|
||||
</template>
|
||||
|
||||
<template v-slot:body>
|
||||
<div v-if="loading" class="loading">
|
||||
<i class="fa fa-circle-o-notch fa-spin fa-3x" :title="$t('loading')"></i>
|
||||
</div>
|
||||
<iframe
|
||||
:src="this.wopiUrl"
|
||||
@load="loaded"
|
||||
></iframe>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</div>
|
||||
<div v-else>
|
||||
<modal v-if="modal.showModal"
|
||||
modalDialogClass="modal-sm"
|
||||
@close="modal.showModal = false">
|
||||
|
||||
<template v-slot:header>
|
||||
<h3>{{ $t('invalid_title') }}</h3>
|
||||
</template>
|
||||
<template v-slot:body>
|
||||
<div class="alert alert-warning">{{ $t('invalid_message') }}</div>
|
||||
</template>
|
||||
|
||||
</modal>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Modal from 'ChillMainAssets/vuejs/_components/Modal';
|
||||
import logo from 'ChillMainAssets/chill/img/logo-chill-sans-slogan_white.png';
|
||||
|
||||
export default {
|
||||
name: "OpenWopiLink",
|
||||
components: {
|
||||
Modal
|
||||
},
|
||||
props: {
|
||||
wopiUrl: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
button: {
|
||||
type: Object,
|
||||
required: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
modal: {
|
||||
showModal: false,
|
||||
modalDialogClass: "modal-fullscreen" //modal-dialog-scrollable
|
||||
},
|
||||
logo: logo,
|
||||
loading: false,
|
||||
mime: [
|
||||
// TODO temporary hardcoded. to be replaced by twig extension or a collabora server query
|
||||
'application/clarisworks',
|
||||
'application/coreldraw',
|
||||
'application/macwriteii',
|
||||
'application/msword',
|
||||
'application/pdf',
|
||||
'application/vnd.lotus-1-2-3',
|
||||
'application/vnd.ms-excel',
|
||||
'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
|
||||
'application/vnd.ms-excel.sheet.macroEnabled.12',
|
||||
'application/vnd.ms-excel.template.macroEnabled.12',
|
||||
'application/vnd.ms-powerpoint',
|
||||
'application/vnd.ms-powerpoint.presentation.macroEnabled.12',
|
||||
'application/vnd.ms-powerpoint.template.macroEnabled.12',
|
||||
'application/vnd.ms-visio.drawing',
|
||||
'application/vnd.ms-word.document.macroEnabled.12',
|
||||
'application/vnd.ms-word.template.macroEnabled.12',
|
||||
'application/vnd.ms-works',
|
||||
'application/vnd.oasis.opendocument.chart',
|
||||
'application/vnd.oasis.opendocument.formula',
|
||||
'application/vnd.oasis.opendocument.graphics',
|
||||
'application/vnd.oasis.opendocument.graphics-flat-xml',
|
||||
'application/vnd.oasis.opendocument.graphics-template',
|
||||
'application/vnd.oasis.opendocument.presentation',
|
||||
'application/vnd.oasis.opendocument.presentation-flat-xml',
|
||||
'application/vnd.oasis.opendocument.presentation-template',
|
||||
'application/vnd.oasis.opendocument.spreadsheet',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-flat-xml',
|
||||
'application/vnd.oasis.opendocument.spreadsheet-template',
|
||||
'application/vnd.oasis.opendocument.text',
|
||||
'application/vnd.oasis.opendocument.text-flat-xml',
|
||||
'application/vnd.oasis.opendocument.text-master',
|
||||
'application/vnd.oasis.opendocument.text-master-template',
|
||||
'application/vnd.oasis.opendocument.text-template',
|
||||
'application/vnd.oasis.opendocument.text-web',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
|
||||
'application/vnd.openxmlformats-officedocument.presentationml.template',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||||
'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
|
||||
'application/vnd.sun.xml.calc',
|
||||
'application/vnd.sun.xml.calc.template',
|
||||
'application/vnd.sun.xml.chart',
|
||||
'application/vnd.sun.xml.draw',
|
||||
'application/vnd.sun.xml.draw.template',
|
||||
'application/vnd.sun.xml.impress',
|
||||
'application/vnd.sun.xml.impress.template',
|
||||
'application/vnd.sun.xml.math',
|
||||
'application/vnd.sun.xml.writer',
|
||||
'application/vnd.sun.xml.writer.global',
|
||||
'application/vnd.sun.xml.writer.template',
|
||||
'application/vnd.visio',
|
||||
'application/vnd.visio2013',
|
||||
'application/vnd.wordperfect',
|
||||
'application/x-abiword',
|
||||
'application/x-aportisdoc',
|
||||
'application/x-dbase',
|
||||
'application/x-dif-document',
|
||||
'application/x-fictionbook+xml',
|
||||
'application/x-gnumeric',
|
||||
'application/x-hwp',
|
||||
'application/x-iwork-keynote-sffkey',
|
||||
'application/x-iwork-numbers-sffnumbers',
|
||||
'application/x-iwork-pages-sffpages',
|
||||
'application/x-mspublisher',
|
||||
'application/x-mswrite',
|
||||
'application/x-pagemaker',
|
||||
'application/x-sony-bbeb',
|
||||
'application/x-t602',
|
||||
]
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isOpenDocument() {
|
||||
if (this.mime.indexOf(this.type) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
noText() {
|
||||
if (typeof this.button.noText !== 'undefined') {
|
||||
return this.button.noText === true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isChangeIcon() {
|
||||
if (typeof this.button.changeIcon !== 'undefined') {
|
||||
return (!(this.button.changeIcon === null || this.button.changeIcon === ''))
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isChangeClass() {
|
||||
if (typeof this.button.changeClass !== 'undefined') {
|
||||
return (!(this.button.changeClass === null || this.button.changeClass === ''))
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openModal() {
|
||||
this.loading = true;
|
||||
this.modal.showModal = true;
|
||||
},
|
||||
loaded() {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
i18n: {
|
||||
messages: {
|
||||
fr: {
|
||||
Update_document: "Modifier le document",
|
||||
save_and_quit: "Enregistrer et quitter",
|
||||
loading: "Chargement de l'éditeur en ligne",
|
||||
invalid_title: "Format incompatible",
|
||||
invalid_message: "Désolé, ce format de document n'est pas éditable en ligne.",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div.wopi-frame {
|
||||
div.modal-header {
|
||||
border-bottom: 0;
|
||||
background-color: var(--bs-primary);
|
||||
color: white;
|
||||
}
|
||||
div.modal-body {
|
||||
padding: 0;
|
||||
overflow-y: unset !important;
|
||||
iframe {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
div.loading {
|
||||
position: absolute;
|
||||
color: var(--bs-chill-gray);
|
||||
top: calc(50% - 30px);
|
||||
left: calc(50% - 30px);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
{% block navigation_search_bar %}{% endblock %}
|
||||
{% block navigation_section_menu %}
|
||||
{{ chill_menu('admin_section', {
|
||||
'layout': '@ChillMain/Menu/adminSection.html.twig',
|
||||
'layout': '@ChillMain/Menu/admin.html.twig',
|
||||
}) }}
|
||||
{% endblock %}
|
||||
|
||||
|
@ -23,23 +23,23 @@
|
||||
|
||||
{% if options['metadata'] %}
|
||||
<div class="metadata">
|
||||
{% if user is not empty %}
|
||||
{{ 'Last updated by'| trans }}
|
||||
<span class="user">
|
||||
{{ user|chill_entity_render_box(options['user']) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if comment.date is not empty %}
|
||||
{% if user is empty %}
|
||||
{{ 'Last updated on'|trans ~ ' ' }}
|
||||
{% else %}
|
||||
{{ 'on'|trans ~ ' ' }}
|
||||
{% endif %}
|
||||
{{ 'Last updated on'|trans ~ ' ' }}
|
||||
<span class="date">
|
||||
{{ comment.date|format_datetime("medium", "short") }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if user is not empty %}
|
||||
{% if comment.date is empty %}
|
||||
{{ 'Last updated by'| trans }}
|
||||
{% else %}
|
||||
{{ 'by_user'|trans ~ ' ' }}
|
||||
{% endif %}
|
||||
<span class="user">
|
||||
{{ user|chill_entity_render_box(options['user']) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</blockquote>
|
||||
{{ closing_box|raw }}
|
||||
{{ closing_box|raw }}
|
@ -216,7 +216,7 @@
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block pick_user_dynamic_widget %}
|
||||
{% block pick_entity_dynamic_widget %}
|
||||
<input type="hidden" {{ block('widget_attributes') }} {% if value is not empty %}value="{{ value }}" {% endif %} data-input-uniqid="{{ form.vars['uniqid'] }}"/>
|
||||
<div data-module="pick-dynamic" data-types="{{ form.vars['types']|json_encode }}" data-multiple="{{ form.vars['multiple'] }}" data-uniqid="{{ form.vars['uniqid'] }}"></div>
|
||||
{% endblock %}
|
||||
|
@ -21,13 +21,13 @@
|
||||
layout ../layoutWithVerticalMenu.html.twig.
|
||||
#}
|
||||
|
||||
<ul class="tab-nav follow-href-path">
|
||||
<li class="title">
|
||||
<div class="list-group vertical-menu {{ 'menu-' ~ menus.name }}">
|
||||
<a class="list-group-item title">
|
||||
{% block v_menu_title %}<!-- title of the verticalMenu is empty -->{% endblock %}
|
||||
</li>
|
||||
</a>
|
||||
{% for menu in menus %}
|
||||
<li class="{% if menu is knp_menu_current %}current {% endif %}">
|
||||
<a href="{{ menu.uri }}" >{{ menu.label|trans }}</a>
|
||||
</li>
|
||||
<a class="list-group-item list-group-item-action" href="{{ menu.uri }}">
|
||||
{{ menu.label|upper }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
|
@ -2,7 +2,7 @@
|
||||
<div class="item-row title">
|
||||
<h2 class="notification-title">
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">
|
||||
{{ 'notification.object_prefix'|trans ~ c.notification.title }}
|
||||
{{ c.notification.title }}
|
||||
</a>
|
||||
</h2>
|
||||
</div>
|
||||
@ -58,14 +58,15 @@
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="notification-content">
|
||||
{% 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 }}
|
||||
<p class="read-more"><a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}">{{ 'Read more'|trans }}</a></p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if c.action_button is not defined or c.action_button != 'false' %}
|
||||
{% if c.action_button is not defined or c.action_button != false %}
|
||||
<div class="item-row separator">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
@ -85,7 +86,13 @@
|
||||
{% if is_granted('CHILL_MAIN_NOTIFICATION_SEE', c.notification) %}
|
||||
<li>
|
||||
<a href="{{ chill_path_add_return_path('chill_main_notification_show', {'id': c.notification.id}) }}"
|
||||
class="btn btn-show change-icon" title="{{ 'notification.see_comments_thread'|trans }}"><i class="fa fa-comment"></i></a>
|
||||
class="btn {% if not c.notification.isSystem %}btn-show change-icon{% else %}btn-misc{% endif %}" title="{{ 'notification.see_comments_thread'|trans }}">
|
||||
{% if not c.notification.isSystem() %}
|
||||
<i class="fa fa-comment"></i>
|
||||
{% else %}
|
||||
{{ 'Read more'|trans }}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
@ -95,7 +102,7 @@
|
||||
|
||||
<div class="item-bloc notification-status {% if notification.isReadBy(app.user) %}read{% else %}unread{% endif %}">
|
||||
|
||||
{% if fold_item is defined and fold_item != 'false' %}
|
||||
{% if fold_item is defined and fold_item != false %}
|
||||
<div class="accordion-header" id="flush-heading-{{ notification.id }}">
|
||||
<button type="button" class="accordion-button collapsed"
|
||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ notification.id }}"
|
||||
|
@ -21,8 +21,6 @@
|
||||
{{ form_row(form.title, { 'label': 'notification.subject'|trans }) }}
|
||||
{{ form_row(form.addressees, { 'label': 'notification.sent_to'|trans }) }}
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
<div class="mb-3 row">
|
||||
<label class="col-form-label col-sm-4" for="notification_message">{{ form_label(form.message) }}</label>
|
||||
<div class="col-12">
|
||||
@ -30,6 +28,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% include handler.template(notification) with handler.templateData(notification) %}
|
||||
|
||||
{{ form_end(form) }}
|
||||
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
|
@ -0,0 +1,12 @@
|
||||
<div class="notification-counter">
|
||||
{% if counter.total > 0 %}
|
||||
<span>
|
||||
{{ 'notification.counter total notifications'|trans({'total': counter.total }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if counter.unread > 0 %}
|
||||
<span>
|
||||
{{ 'notification.counter unread notifications'|trans({'unread': counter.unread }) }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
@ -18,7 +18,7 @@
|
||||
data-notification-id="{{ notification.id }}"
|
||||
data-notification-current-is-read="{{ notification.isReadBy(app.user) }}"
|
||||
data-container="notification-status"
|
||||
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}) }}"
|
||||
data-show-button-url="{{ chill_path_add_return_path('chill_main_notification_show', {'id': notification.id}, false) }}"
|
||||
data-button-class="btn-outline-primary"
|
||||
data-button-text="false"
|
||||
></span>
|
||||
|
@ -50,7 +50,7 @@
|
||||
{% for data in datas %}
|
||||
{% set notification = data.notification %}
|
||||
{% include 'ChillMainBundle:Notification:_list_item.html.twig' with {
|
||||
'fold_item': 'true'
|
||||
'fold_item': true
|
||||
} %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -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
|
||||
} %}
|
||||
</div>
|
||||
|
||||
|
@ -0,0 +1,120 @@
|
||||
{# TODO Adapt condition #}
|
||||
{% if random(1) == 0 %}
|
||||
|
||||
{# For a document #}
|
||||
<h2>{{ 'Document'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="row justify-content-center mt-5">
|
||||
<div class="col-2">
|
||||
<i class="fa fa-4x fa-file-text-o text-success"></i>
|
||||
</div>
|
||||
<div class="col-8">
|
||||
<h3>Imprimé unique, parcours n°14635</h3>
|
||||
<small>Document PDF (6.2 Mo)</small>
|
||||
<p class="mt-2">
|
||||
Description du document. Sed euismod nisi porta lorem mollis aliquam. Non curabitur gravida arcu ac tortor.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
|
||||
{# For an action #}
|
||||
<h2>{{ 'Accompanying Course Action'|trans ~ 'target'|trans }}</h2>
|
||||
|
||||
<div class="flex-table accompanying_course_work-list">
|
||||
{# dynamic insertion
|
||||
::: TODO delete all static insertion, remove condition and pass work object in inclusion
|
||||
#}{% if dynamic is defined %}
|
||||
|
||||
{% set work = '<pass work object here>' %}
|
||||
{% include '@ChillPerson/AccompanyingCourseWork/_item.html.twig' with { 'w': work } %}
|
||||
|
||||
{% else %}
|
||||
|
||||
{# BEGIN static insertion #}
|
||||
<div class="item-bloc">
|
||||
<div class="item-row">
|
||||
<h2 class="badge-title">
|
||||
<span class="title_label"></span>
|
||||
<span class="title_action">Exercer un AEB > Conclure l'AEB
|
||||
<ul class="small_in_title columns mt-1">
|
||||
<li><span class="item-key">Date de début : </span><b>25/11/2021</b></li>
|
||||
<li><span class="item-key">Date de fin : </span><b>10/03/2022</b></li>
|
||||
</ul>
|
||||
</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="wrap-list">
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Référent</h3></div>
|
||||
<div class="wl-col list"><p class="wl-item">Fred</p></div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Usagers du parcours</h3></div>
|
||||
<div class="wl-col list"><span class="wl-item">
|
||||
<span class="onthefly-container" data-target-name="person" data-target-id="1937" data-action="show" data-button-text="Vernon SUBUTEX" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Vernon SUBUTEX</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
<span class="wl-item"><span class="onthefly-container" data-target-name="person" data-target-id="1941" data-action="show" data-button-text="Juan RAMON" data-display-badge="true" data-v-app=""><a data-v-0c1a1125=""><span class="chill-entity entity-person badge-person" data-v-0c1a1125="">Juan RAMON</span></a><!--teleport start--><!--teleport end--></span></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wl-row">
|
||||
<div class="wl-col title"><h3>Problématique sociale</h3></div>
|
||||
<div class="wl-col list">
|
||||
<p class="wl-item social-issues">
|
||||
<span class="chill-entity entity-social-issue"><span class="badge bg-chill-l-gray text-dark"><span class="parent-0">AD - PREVENTION, ACCES AUX DROITS, BUDGET ></span><span class="child">SOUTIEN EQUILIBRE BUDGET</span></span></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-row column">
|
||||
<table class="obj-res-eval my-3">
|
||||
<thead>
|
||||
<tr><th class="obj"><h4 class="title_label">Objectif - motif - dispositif</h4></th>
|
||||
<th class="res"><h4 class="title_label">Résultats - orientations</h4></th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="obj">
|
||||
<p class="chill-no-data-statement">Aucun objectif - motif - dispositif</p>
|
||||
</td>
|
||||
<td class="res">
|
||||
<ul class="result_list">
|
||||
<li>Résultat : Arrêt à l'initiative du ménage pour déménagement</li>
|
||||
<li>Orientation vers une MASP</li>
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="item-row separator">
|
||||
<div class="updatedBy">
|
||||
Dernière mise à jour par
|
||||
<b><span class="chill-entity entity-user">Fred<span class="user-job">(Responsable tous les territoires)</span><span class="main-scope">(ASE)</span></span></b>,<br>
|
||||
le 3 décembre 2021 à 15:19
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# END static insertion #}
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="button" class="btn btn-misc">
|
||||
<i class="fa fa-download fa-fw"></i>{{ 'Download'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
{% set x = random(1) %}
|
||||
<button class="btn btn-update change-icon {% if x == 1 %}disabled{% endif %}">
|
||||
<i class="fa fa-fw fa-{% if x == 0 %}un{% endif %}lock"></i>
|
||||
{{ 'Edit'|trans }}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
@ -0,0 +1,13 @@
|
||||
<h2>{{ 'Join a comment'|trans }}</h2>
|
||||
|
||||
{{ form_start(comment_form) }}
|
||||
|
||||
{{ form_widget(comment_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(comment_form) }}
|
@ -0,0 +1,48 @@
|
||||
<h2>{{ 'Decision'|trans }}</h2>
|
||||
|
||||
{% if transition_form is not null %}
|
||||
{{ form_start(transition_form) }}
|
||||
|
||||
{{ form_row(transition_form.transition) }}
|
||||
|
||||
<div id="finalizeAfter">
|
||||
{{ form_row(transition_form.finalizeAfter) }}
|
||||
</div>
|
||||
|
||||
{% if transition_form.freezeAfter is defined %}
|
||||
{{ form_row(transition_form.freezeAfter) }}
|
||||
{% endif %}
|
||||
|
||||
<div id="futureDestUsers">
|
||||
{{ form_row(transition_form.future_dest_users) }}
|
||||
</div>
|
||||
|
||||
<p>{{ form_label(transition_form.comment) }}</p>
|
||||
|
||||
{{ form_widget(transition_form.comment) }}
|
||||
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<button type="submit" class="btn btn-save">{{ 'Save'|trans }}</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{{ form_end(transition_form) }}
|
||||
{% else %}
|
||||
<div class="alert alert-chill-yellow">
|
||||
|
||||
{% if entity_workflow.currentStep.isFinalizeAfter %}
|
||||
<p>{{ 'workflow.This workflow is finalized'|trans }}</p>
|
||||
{% else %}
|
||||
<p>{{ 'workflow.You are not allowed to apply a transition on this workflow'|trans }}</p>
|
||||
<p>{{ 'workflow.Only those users are allowed'|trans }}:</p>
|
||||
|
||||
<ul>
|
||||
{% for u in entity_workflow.currentStep.destUser -%}
|
||||
<li>{{ u|chill_entity_render_box }}</li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endif %}
|
@ -0,0 +1,13 @@
|
||||
{% if is_granted('CHILL_MAIN_WORKFLOW_CREATE', blank_workflow) %}
|
||||
{# vue component #}
|
||||
<div data-pick-workflow="1"
|
||||
data-related-entity-class="{{ blank_workflow.relatedEntityClass }}"
|
||||
data-related-entity-id="{{ blank_workflow.relatedEntityId }}"
|
||||
data-workflows-availables="{{ workflows_availables|json_encode()|e('html_attr') }}"
|
||||
></div>
|
||||
{% endif %}
|
||||
|
||||
{% if entity_workflows|length > 0 %}
|
||||
{# vue component #}
|
||||
<div data-list-workflows="1" data-workflows="{{ entity_workflows_json|json_encode|e('html_attr') }}"></div>
|
||||
{% endif %}
|
@ -0,0 +1,8 @@
|
||||
<h2>{{ 'Follow workflow'|trans }}</h2>
|
||||
|
||||
{# vue component #}
|
||||
<div data-entity-workflow-subscribe="1"
|
||||
data-entity-workflow-id="{{ entity_workflow.id }}"
|
||||
data-subscribe-step="{{ entity_workflow.isUserSubscribedToStep(app.user)|e('html_attr') }}"
|
||||
data-subscribe-final="{{ entity_workflow.isUserSubscribedToFinal(app.user)|e('html_attr') }}"
|
||||
></div>
|
@ -0,0 +1,59 @@
|
||||
<h2>{{ 'Workflow history'|trans }}</h2>
|
||||
|
||||
<div class="flex-table">
|
||||
{% for step in entity_workflow.stepsChained %}
|
||||
<div class="item-bloc {{ 'bloc' ~ step.id }} {% if loop.first %}initial{% endif %}">
|
||||
<div class="item-row">
|
||||
{% if loop.first and step.next is null %}
|
||||
<div class="item-col">
|
||||
{{ 'workflow.No transitions'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="decided">
|
||||
{% if not loop.first %}
|
||||
<i class="fa fa-check fa-fw text-success"></i>
|
||||
{% endif %}
|
||||
{{ step.currentStep }}
|
||||
</div>
|
||||
{#
|
||||
<div class="decided">
|
||||
<i class="fa fa-times fa-fw text-danger"></i>
|
||||
Refusé
|
||||
</div>
|
||||
#}
|
||||
</div>
|
||||
</div>
|
||||
{% if step.next is not null %}
|
||||
<div class="item-row separator">
|
||||
<div class="item-col" style="width: inherit;">
|
||||
{% if step.transitionBy is not null %}
|
||||
<div>
|
||||
{{ step.transitionBy|chill_entity_render_box }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<span>{{ step.transitionAt|format_datetime('long', 'medium') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="item-col flex-column align-items-end">
|
||||
<div class="to-decision">
|
||||
<i class="fa fa-share fa-fw text-secondary" title="transféré"></i>
|
||||
{{ step.next.currentStep }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if step.comment is not empty %}
|
||||
<div class="item-row separator">
|
||||
<blockquote class="chill-user-quote col">
|
||||
{{ step.comment|chill_markdown_to_html }}
|
||||
</blockquote>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</div>
|
@ -0,0 +1,12 @@
|
||||
<div class="item-row col">
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
{% include handler.templateTitle(l.entity_workflow) with handler.templateTitleData(entity_workflow)|merge({
|
||||
'description': true,
|
||||
'breadcrumb': true,
|
||||
'add_classes': 'ms-3 h3'
|
||||
}) %}
|
||||
|
||||
|
||||
</div>
|
@ -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 %}
|
||||
<div class="col-10 workflow">
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
{# 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
|
||||
#}
|
||||
<section class="step my-4">
|
||||
<div class="mb-5">
|
||||
{% include handler_template_title with handler_template_data|merge({'breadcrumb': true }) %}
|
||||
</div>
|
||||
{% include handler_template with handler_template_data|merge({'display_action': true }) %}
|
||||
</section>
|
||||
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_follow.html.twig' %}</section>
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_decision.html.twig' %}</section>{#
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_comment.html.twig' %}</section> #}
|
||||
<section class="step my-4">{% include '@ChillMain/Workflow/_history.html.twig' %}</section>
|
||||
|
||||
{# useful ?
|
||||
<ul class="record_actions sticky-form-buttons">
|
||||
<li class="cancel">
|
||||
<a class="btn btn-cancel" href="{{ path('chill_main_workflow_list_dest') }}">
|
||||
{{ 'Back to the list'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
#}
|
||||
</div>
|
||||
{% endblock %}
|
@ -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 %}
|
||||
<div class="col-10 workflow">
|
||||
|
||||
<h1 class="mb-5">{{ block('title') }}</h1>
|
||||
|
||||
<ul class="nav nav-pills justify-content-center">
|
||||
<li class="nav-item">
|
||||
<a href="{{ path('chill_main_workflow_list_subscribed') }}"
|
||||
class="nav-link {% if step == 'subscribed' %}active{% endif %}">
|
||||
{{ 'workflow.subscribed'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a href="{{ path('chill_main_workflow_list_dest') }}"
|
||||
class="nav-link {% if step == 'dest' %}active{% endif %}">
|
||||
{{ 'workflow.dest'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
{% if workflows|length == 0 %}
|
||||
<p class="chill-no-data-statement">{{ 'workflow.No workflow'|trans }}</p>
|
||||
{% else %}
|
||||
<div class="flex-table accordion accordion-flush" id="workflow-fold">
|
||||
{% for l in workflows %}
|
||||
<div class="item-bloc">
|
||||
<div class="accordion-header" id="flush-heading-{{ l.entity_workflow.id }}">
|
||||
<button type="button" class="accordion-button collapsed"
|
||||
data-bs-toggle="collapse" data-bs-target="#flush-collapse-{{ l.entity_workflow.id }}"
|
||||
aria-expanded="false" aria-controls="flush-collapse-{{ l.entity_workflow.id }}">
|
||||
|
||||
<div class="item-row col">
|
||||
<h2>
|
||||
{{ 'workflow_'|trans }}
|
||||
</h2>
|
||||
{% include l.handler.templateTitle(l.entity_workflow) with l.handler.templateTitleData(l.entity_workflow)|merge({
|
||||
'description': true,
|
||||
'add_classes': 'ms-3 h3'
|
||||
}) %}
|
||||
</div>
|
||||
|
||||
</button>
|
||||
{{ macro.breadcrumb(l) }}
|
||||
|
||||
</div>
|
||||
<div id="flush-collapse-{{ l.entity_workflow.id }}"
|
||||
class="accordion-collapse collapse"
|
||||
aria-labelledby="flush-heading-{{ l.entity_workflow.id }}"
|
||||
data-bs-parent="#workflow-fold">
|
||||
|
||||
<div class="item-row flex-column">
|
||||
{% include l.handler.template(l.entity_workflow) with l.handler.templateData(l.entity_workflow)|merge({
|
||||
'display_action': false
|
||||
}) %}
|
||||
</div>
|
||||
<div class="item-row">
|
||||
<div class="item-col flex-grow-1">
|
||||
<p>
|
||||
{% if l.entity_workflow.isUserSubscribedToStep(app.user) %}
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ 'workflow.you subscribed to all steps'|trans }}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p>
|
||||
{% if l.entity_workflow.isUserSubscribedToFinal(app.user) %}
|
||||
<i class="fa fa-check fa-fw"></i>
|
||||
{{ 'workflow.you subscribed to final step'|trans }}
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="item-col">
|
||||
<ul class="record_actions">
|
||||
<li>
|
||||
<a href="{{ path('chill_main_workflow_show', {'id': l.entity_workflow.id}) }}"
|
||||
class="btn btn-show">
|
||||
{{ 'Show'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,45 @@
|
||||
{% macro popoverContent(step) %}
|
||||
<ul class="small_in_title">
|
||||
<li>
|
||||
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.transitionBy|chill_entity_render_box }}</b>
|
||||
</li>
|
||||
<li>
|
||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||
<b>{{ step.transitionAt|format_datetime('short', 'short') }}</b>
|
||||
</li>
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro breadcrumb(_ctx) %}
|
||||
<div class="breadcrumb">
|
||||
{% for step in _ctx.entity_workflow.stepsChained %}
|
||||
{% if step.previous is null %}
|
||||
{#
|
||||
{% set popContent = "Point de départ du workflow" %}
|
||||
{{ dump(step) }}
|
||||
#}
|
||||
{% set popContent = _self.popoverContent(step) %}
|
||||
{% else %}
|
||||
{% set popContent = _self.popoverContent(step.previous) %}
|
||||
{% endif %}
|
||||
<span class="mx-2"
|
||||
tabindex="0"
|
||||
data-bs-trigger="focus hover"
|
||||
data-bs-toggle="popover"
|
||||
data-bs-placement="bottom"
|
||||
data-bs-custom-class="workflow-transition"
|
||||
title="{{ step.currentStep }}"
|
||||
data-bs-content="{{ popContent|e('html_attr') }}"
|
||||
>
|
||||
{% if step.currentStep == 'initial' %}
|
||||
<i class="fa fa-circle me-1 text-chill-yellow"></i>
|
||||
{% endif %}
|
||||
{{ step.currentStep }}
|
||||
</span>
|
||||
{% if not loop.last %}
|
||||
→
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endmacro %}
|
@ -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,
|
@ -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 -%}
|
@ -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',
|
||||
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Security\Authorization;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
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 EntityWorkflowVoter extends Voter
|
||||
{
|
||||
public const CREATE = 'CHILL_MAIN_WORKFLOW_CREATE';
|
||||
|
||||
public const SEE = 'CHILL_MAIN_WORKFLOW_SEE';
|
||||
|
||||
private EntityWorkflowManager $manager;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityWorkflowManager $manager, Security $security)
|
||||
{
|
||||
$this->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,
|
||||
];
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Serializer\Normalizer;
|
||||
|
||||
use Chill\MainBundle\Entity\Embeddable\CommentEmbeddable;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Repository\UserRepository;
|
||||
use DateTime;
|
||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||
use function array_key_exists;
|
||||
|
||||
class CommentEmbeddableDocGenNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface
|
||||
{
|
||||
use NormalizerAwareTrait;
|
||||
|
||||
private UserRepository $userRepository;
|
||||
|
||||
public function __construct(UserRepository $userRepository)
|
||||
{
|
||||
$this->userRepository = $userRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param CommentEmbeddable $object
|
||||
*
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function normalize($object, ?string $format = null, array $context = []): array
|
||||
{
|
||||
if (null === $object) {
|
||||
return [
|
||||
'comment' => '',
|
||||
'isNull' => true,
|
||||
'date' => $this->normalizer->normalize(null, $format, array_merge($context, [
|
||||
'docgen:expects' => DateTime::class,
|
||||
])),
|
||||
'user' => $this->normalizer->normalize(null, $format, array_merge($context, [
|
||||
'docgen:expects' => User::class,
|
||||
])),
|
||||
];
|
||||
}
|
||||
|
||||
$user = $this->userRepository->find($object->getUserId());
|
||||
|
||||
return [
|
||||
'comment' => (string) $object->getComment(),
|
||||
'isNull' => false,
|
||||
'date' => $this->normalizer->normalize($object->getDate(), $format, array_merge($context, [
|
||||
'docgen:expects' => DateTime::class,
|
||||
])),
|
||||
'user' => $this->normalizer->normalize($user, $format, array_merge($context, [
|
||||
'docgen:expects' => User::class,
|
||||
])),
|
||||
];
|
||||
}
|
||||
|
||||
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
|
||||
{
|
||||
if ('docgen' !== $format) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($data instanceof CommentEmbeddable) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
null === $data
|
||||
&& array_key_exists('docgen:expects', $context)
|
||||
&& CommentEmbeddable::class === $context['docgen:expects']) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
50
src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php
Normal file
50
src/Bundle/ChillMainBundle/Service/Mailer/ChillMailer.php
Normal file
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Service\Mailer;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Envelope;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Mime\RawMessage;
|
||||
|
||||
class ChillMailer implements MailerInterface
|
||||
{
|
||||
private LoggerInterface $chillLogger;
|
||||
|
||||
private MailerInterface $initial;
|
||||
|
||||
private string $prefix = '[Chill] ';
|
||||
|
||||
public function __construct(MailerInterface $initial, LoggerInterface $chillLogger)
|
||||
{
|
||||
$this->initial = $initial;
|
||||
$this->chillLogger = $chillLogger;
|
||||
}
|
||||
|
||||
public function send(RawMessage $message, ?Envelope $envelope = null): void
|
||||
{
|
||||
if ($message instanceof Email) {
|
||||
$message->subject($this->prefix . $message->getSubject());
|
||||
}
|
||||
|
||||
$this->chillLogger->info('chill email sent', [
|
||||
'to' => array_map(static function (Address $address) {
|
||||
return $address->getAddress();
|
||||
}, $message->getTo()),
|
||||
'subject' => $message->getSubject(),
|
||||
]);
|
||||
|
||||
$this->initial->send($message, $envelope);
|
||||
}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Tests\Entity\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
* @coversNothing
|
||||
*/
|
||||
final class EntityWorkflowTest extends TestCase
|
||||
{
|
||||
public function testIsFinalizeWith1Steps()
|
||||
{
|
||||
$entityWorkflow = new EntityWorkflow();
|
||||
|
||||
$entityWorkflow->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());
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
|
||||
interface EntityWorkflowHandlerInterface
|
||||
{
|
||||
public function getRelatedEntity(EntityWorkflow $entityWorkflow): ?object;
|
||||
|
||||
/**
|
||||
* Return a string representing the role required for seeing the workflow.
|
||||
*
|
||||
* Return Null if any check is required, or a role name. The voter will check for
|
||||
* authorization with the role as attribute, and the
|
||||
*/
|
||||
public function getRoleShow(EntityWorkflow $entityWorkflow): ?string;
|
||||
|
||||
public function getTemplate(EntityWorkflow $entityWorkflow, array $options = []): string;
|
||||
|
||||
public function getTemplateData(EntityWorkflow $entityWorkflow, array $options = []): array;
|
||||
|
||||
public function getTemplateTitle(EntityWorkflow $entityWorkflow, array $options = []): string;
|
||||
|
||||
public function getTemplateTitleData(EntityWorkflow $entityWorkflow, array $options = []): array;
|
||||
|
||||
public function supports(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||
|
||||
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool;
|
||||
}
|
@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\Exception\HandlerNotFoundException;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
|
||||
class EntityWorkflowManager
|
||||
{
|
||||
/**
|
||||
* @var iterable|EntityWorkflowHandlerInterface[]
|
||||
*/
|
||||
private iterable $handlers;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
public function __construct(iterable $handlers, Registry $registry)
|
||||
{
|
||||
$this->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);
|
||||
}
|
||||
}
|
@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||
use DateTimeImmutable;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||
use Symfony\Component\Workflow\TransitionBlocker;
|
||||
|
||||
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private LoggerInterface $chillLogger;
|
||||
|
||||
private Security $security;
|
||||
|
||||
private UserRender $userRender;
|
||||
|
||||
public function __construct(
|
||||
LoggerInterface $chillLogger,
|
||||
Security $security,
|
||||
UserRender $userRender
|
||||
) {
|
||||
$this->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(),
|
||||
]);
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\EventSubscriber;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Security;
|
||||
use Symfony\Component\Templating\EngineInterface;
|
||||
use Symfony\Component\Workflow\Event\Event;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use function in_array;
|
||||
|
||||
class NotificationOnTransition implements EventSubscriberInterface
|
||||
{
|
||||
private EngineInterface $engine;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
|
||||
private MetadataExtractor $metadataExtractor;
|
||||
|
||||
private Registry $registry;
|
||||
|
||||
private Security $security;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager, EngineInterface $engine, MetadataExtractor $metadataExtractor, Security $security, Registry $registry)
|
||||
{
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class HandlerNotFoundException extends RuntimeException
|
||||
{
|
||||
}
|
@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Helper;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||
use Symfony\Component\Workflow\Registry;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
use function array_key_exists;
|
||||
|
||||
class MetadataExtractor
|
||||
{
|
||||
private Registry $registry;
|
||||
|
||||
private TranslatableStringHelperInterface $translatableStringHelper;
|
||||
|
||||
public function __construct(Registry $registry, TranslatableStringHelperInterface $translatableStringHelper)
|
||||
{
|
||||
$this->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];
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow\Notification;
|
||||
|
||||
use Chill\MainBundle\Entity\Notification;
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Chill\MainBundle\Notification\NotificationHandlerInterface;
|
||||
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||
|
||||
class WorkflowNotificationHandler implements NotificationHandlerInterface
|
||||
{
|
||||
private EntityWorkflowManager $entityWorkflowManager;
|
||||
|
||||
private EntityWorkflowRepository $entityWorkflowRepository;
|
||||
|
||||
public function getTemplate(Notification $notification, array $options = []): string
|
||||
{
|
||||
return '@ChillMain/Workflow/_notification_include.html.twig';
|
||||
}
|
||||
|
||||
public function getTemplateData(Notification $notification, array $options = []): array
|
||||
{
|
||||
$entityWorkflow = $this->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;
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* For the full copyright and license information, please view
|
||||
* the LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Chill\MainBundle\Workflow;
|
||||
|
||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||
use Symfony\Component\Workflow\SupportStrategy\WorkflowSupportStrategyInterface;
|
||||
use Symfony\Component\Workflow\WorkflowInterface;
|
||||
|
||||
class RelatedEntityWorkflowSupportsStrategy implements WorkflowSupportStrategyInterface
|
||||
{
|
||||
public function supports(WorkflowInterface $workflow, $subject): bool
|
||||
{
|
||||
if (!$subject instanceof EntityWorkflow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($workflow->getMetadataStore()->getWorkflowMetadata()['related_entity']
|
||||
as $relatedEntityClass) {
|
||||
if ($subject->getRelatedEntityClass() === $relatedEntityClass) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user