mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-28 13:06:13 +00:00
Merge signature-app-master into branch
This commit is contained in:
commit
790576863f
21
.changes/v2.20.0.md
Normal file
21
.changes/v2.20.0.md
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
## v2.20.0 - 2024-06-05
|
||||||
|
### Fixed
|
||||||
|
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
|
||||||
|
* Added translations for choices of durations (> 5 hours)
|
||||||
|
### Feature
|
||||||
|
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
|
||||||
|
|
||||||
|
This endpoint should be added to make the endpoint works properly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
firewalls:
|
||||||
|
dav:
|
||||||
|
pattern: ^/dav
|
||||||
|
provider: chain_provider
|
||||||
|
stateless: true
|
||||||
|
guard:
|
||||||
|
authenticators:
|
||||||
|
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
|
||||||
|
|
||||||
|
```
|
3
.changes/v2.20.1.md
Normal file
3
.changes/v2.20.1.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## v2.20.1 - 2024-06-05
|
||||||
|
### Fixed
|
||||||
|
* Do not allow StoredObjectCreated for edit and convert buttons
|
31
.changes/v2.21.0.md
Normal file
31
.changes/v2.21.0.md
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
## v2.21.0 - 2024-06-18
|
||||||
|
### Feature
|
||||||
|
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
|
||||||
|
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
|
||||||
|
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
|
||||||
|
now, the job and service is shown:
|
||||||
|
* at the activity's date,
|
||||||
|
* at the appointment's date,
|
||||||
|
* when the user is marked as referrer for an accompanying period work,
|
||||||
|
* when the user apply a transition in a workflow,
|
||||||
|
* when the user updates or creates "something" ("created/updated by ... at ..."),
|
||||||
|
* or when he wrote a comment,
|
||||||
|
* …
|
||||||
|
|
||||||
|
### Traduction francophone
|
||||||
|
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
|
||||||
|
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
|
||||||
|
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
|
||||||
|
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
|
||||||
|
* à la date d'un échange,
|
||||||
|
* au jour d'un rendez-vous,
|
||||||
|
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
|
||||||
|
* quand il a appliqué une transition sur un workflow,
|
||||||
|
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
|
||||||
|
* quand il a mis à jour un commentaire,
|
||||||
|
* …
|
||||||
|
|
58
CHANGELOG.md
58
CHANGELOG.md
@ -6,6 +6,64 @@ adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
|
|||||||
and is generated by [Changie](https://github.com/miniscruff/changie).
|
and is generated by [Changie](https://github.com/miniscruff/changie).
|
||||||
|
|
||||||
|
|
||||||
|
## v2.21.0 - 2024-06-18
|
||||||
|
### Feature
|
||||||
|
* Add flash menu buttons in search results, to open directly a new calendar, or a new activity in an accompanying period
|
||||||
|
* ([#122](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/122)) Improve the list of calendar in the search results: make all calendar clicable, and display a list of calendars
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add start date and end date on filters "filter course by referrer job" and "filter course by referrer scope"
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] the aggregator "Group by referrer" now accept a date range.
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's scope"
|
||||||
|
* ([#282](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/282)) [export] add date range on "group course by referrer's jobs"
|
||||||
|
* ([#168](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/168) In the UX, display user job and service at the time when he performs an action:
|
||||||
|
now, the job and service is shown:
|
||||||
|
* at the activity's date,
|
||||||
|
* at the appointment's date,
|
||||||
|
* when the user is marked as referrer for an accompanying period work,
|
||||||
|
* when the user apply a transition in a workflow,
|
||||||
|
* when the user updates or creates "something" ("created/updated by ... at ..."),
|
||||||
|
* or when he wrote a comment,
|
||||||
|
* …
|
||||||
|
|
||||||
|
### Traduction francophone
|
||||||
|
* Ajout d'un menu "flash" dans les résultats de recherche, pour créer un rendez-vous ou un échange dans un parcours depuis les résultats de recherche;
|
||||||
|
* Améliore la liste des rendez-vous dans les résultats de recherche: les rendez-vous sont cliquables;
|
||||||
|
* [exports] Ajout d'intervalles de dates pour des filtres et regroupements des parcours par référent, métier du référent, service du référent;
|
||||||
|
* Affiche le métier et le service des utilisateurs à la date à laquelle il a exécuté une action. Le métier et le service est affiché:
|
||||||
|
* à la date d'un échange,
|
||||||
|
* au jour d'un rendez-vous,
|
||||||
|
* quand l'utilisateur est devenu référent d'un parcours d'accompagnement,
|
||||||
|
* quand il a appliqué une transition sur un workflow,
|
||||||
|
* quand il a mise à jour ou créé une fiche, dans les mentions "créé / mise à jour par ..., le ...",
|
||||||
|
* quand il a mis à jour un commentaire,
|
||||||
|
* …
|
||||||
|
|
||||||
|
|
||||||
|
## v2.20.1 - 2024-06-05
|
||||||
|
### Fixed
|
||||||
|
* Do not allow StoredObjectCreated for edit and convert buttons
|
||||||
|
|
||||||
|
## v2.20.0 - 2024-06-05
|
||||||
|
### Fixed
|
||||||
|
* ([#170](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/170)) Display agents traitants instead of accompanying period referrer in export list social actions.
|
||||||
|
* Added translations for choices of durations (> 5 hours)
|
||||||
|
### Feature
|
||||||
|
* ([#145](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/145)) Allow to open documents in LibreOffice locally (need configuration within security);
|
||||||
|
|
||||||
|
This endpoint should be added to make the endpoint works properly:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
security:
|
||||||
|
firewalls:
|
||||||
|
dav:
|
||||||
|
pattern: ^/dav
|
||||||
|
provider: chain_provider
|
||||||
|
stateless: true
|
||||||
|
guard:
|
||||||
|
authenticators:
|
||||||
|
- Chill\DocStoreBundle\Security\Guard\JWTOnDavUrlAuthenticator
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
## v2.19.0 - 2024-05-14
|
## v2.19.0 - 2024-05-14
|
||||||
### Feature
|
### Feature
|
||||||
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side
|
* ([#197](https://gitlab.com/Chill-Projet/chill-bundles/-/issues/197)) Make the script which subscribe to microsoft calendars changes more tolerant to errors or missing configuration on the microsoft side
|
||||||
|
@ -19,7 +19,6 @@
|
|||||||
"doctrine/doctrine-migrations-bundle": "^3.0",
|
"doctrine/doctrine-migrations-bundle": "^3.0",
|
||||||
"doctrine/orm": "^2.13.0",
|
"doctrine/orm": "^2.13.0",
|
||||||
"erusev/parsedown": "^1.7",
|
"erusev/parsedown": "^1.7",
|
||||||
"graylog2/gelf-php": "^1.5",
|
|
||||||
"knplabs/knp-menu-bundle": "^3.0",
|
"knplabs/knp-menu-bundle": "^3.0",
|
||||||
"knplabs/knp-time-bundle": "^1.12",
|
"knplabs/knp-time-bundle": "^1.12",
|
||||||
"knpuniversity/oauth2-client-bundle": "^2.10",
|
"knpuniversity/oauth2-client-bundle": "^2.10",
|
||||||
@ -93,11 +92,12 @@
|
|||||||
"phpstan/phpstan": "^1.9",
|
"phpstan/phpstan": "^1.9",
|
||||||
"phpstan/phpstan-deprecation-rules": "^1.1",
|
"phpstan/phpstan-deprecation-rules": "^1.1",
|
||||||
"phpstan/phpstan-strict-rules": "^1.0",
|
"phpstan/phpstan-strict-rules": "^1.0",
|
||||||
"phpunit/phpunit": ">= 7.5",
|
"phpunit/phpunit": "^10.5.24",
|
||||||
"rector/rector": "^1.1.0",
|
"rector/rector": "^1.1.0",
|
||||||
"symfony/debug-bundle": "^5.4",
|
"symfony/debug-bundle": "^5.4",
|
||||||
"symfony/dotenv": "^5.4",
|
"symfony/dotenv": "^5.4",
|
||||||
"symfony/maker-bundle": "^1.20",
|
"symfony/maker-bundle": "^1.20",
|
||||||
|
"symfony/phpunit-bridge": "^7.1",
|
||||||
"symfony/runtime": "^5.4",
|
"symfony/runtime": "^5.4",
|
||||||
"symfony/stopwatch": "^5.4",
|
"symfony/stopwatch": "^5.4",
|
||||||
"symfony/var-dumper": "^5.4"
|
"symfony/var-dumper": "^5.4"
|
||||||
|
@ -95,7 +95,7 @@ custom developments. But most of the time, this should be fine.
|
|||||||
|
|
||||||
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
|
You have to configure some local variables, which are described in the :code:`.env` file. The secrets should not be stored
|
||||||
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
|
in this :code:`.env` file, but instead using the `secrets management tool <https://symfony.com/doc/current/configuration/secrets.html>`_
|
||||||
or in the :code:`.env.local` file, which should not be commited to the git repository.
|
or in the :code:`.env.local` file, which should not be committed to the git repository.
|
||||||
|
|
||||||
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
|
You do not need to set variables for the smtp server, redis server and relatorio server, as they are generated automatically
|
||||||
by the symfony server, from the docker compose services.
|
by the symfony server, from the docker compose services.
|
||||||
@ -114,6 +114,12 @@ you can either:
|
|||||||
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
|
- add the generated password to the secrets manager (**note**: you must add the generated hashed password to the secrets env,
|
||||||
not the password in clear text).
|
not the password in clear text).
|
||||||
|
|
||||||
|
- set up the jwt authentication bundle
|
||||||
|
|
||||||
|
Some environment variables are available for the JWT authentication bundle in the :code:`.env` file. You must also run the command
|
||||||
|
:code:`symfony console lexik:jwt:generate-keypair` to generate some keys that will be stored in the paths set up in the :code:`JWT_SECRET_KEY`
|
||||||
|
and the :code:`JWT_PUBLIC_KEY` env variables. This is only required for using the stored documents in Chill.
|
||||||
|
|
||||||
Prepare migrations and other tools
|
Prepare migrations and other tools
|
||||||
**********************************
|
**********************************
|
||||||
|
|
||||||
@ -164,7 +170,7 @@ can rely on the whole chill framework, meaning there is no need to add them to t
|
|||||||
You will require some bundles to have the following development tools:
|
You will require some bundles to have the following development tools:
|
||||||
|
|
||||||
- add fixtures
|
- add fixtures
|
||||||
- add profiler and var-dumper to debug
|
- add profiler and debug bundle
|
||||||
|
|
||||||
Install fixtures
|
Install fixtures
|
||||||
****************
|
****************
|
||||||
@ -179,7 +185,7 @@ Install fixtures
|
|||||||
This will generate user accounts, centers, and some basic configuration.
|
This will generate user accounts, centers, and some basic configuration.
|
||||||
|
|
||||||
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
|
The accounts created are: :code:`center a_social`, :code:`center b_social`, :code:`center a_direction`, ... The full list is
|
||||||
visibile in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
|
visible in the "users" table: :code:`docker compose exec database psql -U app -c "SELECT username FROM users"`.
|
||||||
|
|
||||||
The password is always :code:`password`.
|
The password is always :code:`password`.
|
||||||
|
|
||||||
@ -192,7 +198,7 @@ Add web profiler and debugger
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
symfony composer require --dev symfony/web-profiler-bundle symfony/var-dumper
|
symfony composer require --dev symfony/web-profiler-bundle symfony/debug-bundle
|
||||||
|
|
||||||
Working on chill bundles
|
Working on chill bundles
|
||||||
************************
|
************************
|
||||||
|
@ -46,9 +46,11 @@
|
|||||||
"@fullcalendar/vue3": "^6.1.4",
|
"@fullcalendar/vue3": "^6.1.4",
|
||||||
"@popperjs/core": "^2.9.2",
|
"@popperjs/core": "^2.9.2",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/leaflet": "^1.9.3",
|
||||||
|
"@types/dompurify": "^3.0.5",
|
||||||
"dropzone": "^5.7.6",
|
"dropzone": "^5.7.6",
|
||||||
"es6-promise": "^4.2.8",
|
"es6-promise": "^4.2.8",
|
||||||
"leaflet": "^1.7.1",
|
"leaflet": "^1.7.1",
|
||||||
|
"marked": "^12.0.2",
|
||||||
"masonry-layout": "^4.2.2",
|
"masonry-layout": "^4.2.2",
|
||||||
"mime": "^4.0.0",
|
"mime": "^4.0.0",
|
||||||
"swagger-ui": "^4.15.5",
|
"swagger-ui": "^4.15.5",
|
||||||
|
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\ActivityBundle\Menu;
|
||||||
|
|
||||||
|
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
|
||||||
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Knp\Menu\MenuItem;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private Security $security) {}
|
||||||
|
|
||||||
|
public static function getMenuIds(): array
|
||||||
|
{
|
||||||
|
return ['accompanying_course_quick_menu'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
|
{
|
||||||
|
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
|
||||||
|
$accompanyingCourse = $parameters['accompanying-course'];
|
||||||
|
|
||||||
|
if ($this->security->isGranted(ActivityVoter::CREATE, $accompanyingCourse)) {
|
||||||
|
$menu
|
||||||
|
->addChild('Create a new activity in accompanying course', [
|
||||||
|
'route' => 'chill_activity_activity_new',
|
||||||
|
'routeParameters' => [
|
||||||
|
// 'activityType_id' => '',
|
||||||
|
'accompanying_period_id' => $accompanyingCourse->getId(),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->setExtras([
|
||||||
|
'order' => 10,
|
||||||
|
'icon' => 'plus',
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -68,7 +68,7 @@
|
|||||||
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
<div class="wl-col title"><h3>{{ 'Referrer'|trans }}</h3></div>
|
||||||
<div class="wl-col list">
|
<div class="wl-col list">
|
||||||
<p class="wl-item">
|
<p class="wl-item">
|
||||||
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -87,7 +87,8 @@
|
|||||||
<li>
|
<li>
|
||||||
{% if bloc.type == 'user' %}
|
{% if bloc.type == 'user' %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
hello
|
||||||
|
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||||
@ -114,7 +115,7 @@
|
|||||||
<li>
|
<li>
|
||||||
{% if bloc.type == 'user' %}
|
{% if bloc.type == 'user' %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ _self.insert_onthefly(bloc.type, item) }}
|
{{ _self.insert_onthefly(bloc.type, item) }}
|
||||||
@ -142,7 +143,7 @@
|
|||||||
<span class="wl-item">
|
<span class="wl-item">
|
||||||
{% if bloc.type == 'user' %}
|
{% if bloc.type == 'user' %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false }) }}
|
{{ item|chill_entity_render_box({'render': 'raw', 'addAltNames': false, 'at_date': entity.date }) }}
|
||||||
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
|
{%- if context == 'calendar_accompanyingCourse' or context == 'calendar_person' %}
|
||||||
{% set invite = entity.inviteForUser(item) %}
|
{% set invite = entity.inviteForUser(item) %}
|
||||||
{% if invite is not null %}
|
{% if invite is not null %}
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
{% if activity.user and t.userVisible %}
|
{% if activity.user and t.userVisible %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
|
<span class="item-key">{{ 'Referrer'|trans ~ ': ' }}</span>
|
||||||
<span class="badge-user">{{ activity.user|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ activity.user|chill_entity_render_box({'at_date': activity.date}) }}</span>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
{%- if entity.user is not null %}
|
{%- if entity.user is not null %}
|
||||||
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
|
<dt class="inline">{{ 'Referrer'|trans|capitalize }}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<span class="badge-user">{{ entity.user|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ entity.user|chill_entity_render_box({'at_date': entity.date}) }}</span>
|
||||||
</dd>
|
</dd>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -145,7 +145,7 @@ class ActivityVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
|
|||||||
throw new \RuntimeException('Could not determine context of activity.');
|
throw new \RuntimeException('Could not determine context of activity.');
|
||||||
}
|
}
|
||||||
} elseif ($subject instanceof AccompanyingPeriod) {
|
} elseif ($subject instanceof AccompanyingPeriod) {
|
||||||
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
|
if (AccompanyingPeriod::STEP_CLOSED === $subject->getStep() || AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
|
||||||
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
|
if (\in_array($attribute, [self::UPDATE, self::CREATE, self::DELETE], true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -77,6 +77,18 @@ Choose a type: Choisir un type
|
|||||||
4 hours: 4 heures
|
4 hours: 4 heures
|
||||||
4 hours 30: 4 heures 30
|
4 hours 30: 4 heures 30
|
||||||
5 hours: 5 heures
|
5 hours: 5 heures
|
||||||
|
5 hours 30: 5 heure 30
|
||||||
|
6 hours: 6 heures
|
||||||
|
6 hours 30: 6 heure 30
|
||||||
|
7 hours: 7 heures
|
||||||
|
7 hours 30: 7 heure 30
|
||||||
|
8 hours: 8 heures
|
||||||
|
8 hours 30: 8 heure 30
|
||||||
|
9 hours: 9 heures
|
||||||
|
9 hours 30: 9 heure 30
|
||||||
|
10 hours: 10 heures
|
||||||
|
11 hours: 11 heures
|
||||||
|
12 hours: 12 heures
|
||||||
Concerned groups: Parties concernées par l'échange
|
Concerned groups: Parties concernées par l'échange
|
||||||
Persons in accompanying course: Usagers du parcours
|
Persons in accompanying course: Usagers du parcours
|
||||||
Third persons: Tiers non-pro.
|
Third persons: Tiers non-pro.
|
||||||
@ -210,6 +222,7 @@ Documents label: Libellé du champ Documents
|
|||||||
# activity type category admin
|
# activity type category admin
|
||||||
ActivityTypeCategory list: Liste des catégories des types d'échange
|
ActivityTypeCategory list: Liste des catégories des types d'échange
|
||||||
Create a new activity type category: Créer une nouvelle catégorie de type d'échange
|
Create a new activity type category: Créer une nouvelle catégorie de type d'échange
|
||||||
|
Create a new activity in accompanying course: Créer un échange dans le parcours
|
||||||
|
|
||||||
# activity delete
|
# activity delete
|
||||||
Remove activity: Supprimer un échange
|
Remove activity: Supprimer un échange
|
||||||
|
@ -49,13 +49,13 @@
|
|||||||
<li>
|
<li>
|
||||||
<span>
|
<span>
|
||||||
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
|
<abbr class="referrer" title={{ 'Created by'|trans }}>{{ 'By'|trans }}:</abbr>
|
||||||
<b>{{ entity.createdBy|chill_entity_render_box }}</b>
|
<b>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</b>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span>
|
<span>
|
||||||
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
|
<abbr class="referrer" title={{ 'Created for'|trans }}>{{ 'For'|trans }}:</abbr>
|
||||||
<b>{{ entity.agent|chill_entity_render_box }}</b>
|
<b>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</b>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
@ -18,10 +18,10 @@
|
|||||||
<dd>{{ entity.type|chill_entity_render_box }}</dd>
|
<dd>{{ entity.type|chill_entity_render_box }}</dd>
|
||||||
|
|
||||||
<dt class="inline">{{ 'Created by'|trans }}</dt>
|
<dt class="inline">{{ 'Created by'|trans }}</dt>
|
||||||
<dd>{{ entity.createdBy }}</dd>
|
<dd>{{ entity.createdBy|chill_entity_render_box({'at_date': entity.date}) }}</dd>
|
||||||
|
|
||||||
<dt class="inline">{{ 'Created for'|trans }}</dt>
|
<dt class="inline">{{ 'Created for'|trans }}</dt>
|
||||||
<dd>{{ entity.agent }}</dd>
|
<dd>{{ entity.agent|chill_entity_render_box({'at_date': entity.date}) }}</dd>
|
||||||
|
|
||||||
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
|
<dt class="inline">{{ 'Asideactivity location'|trans }}</dt>
|
||||||
{%- if entity.location.name is defined -%}
|
{%- if entity.location.name is defined -%}
|
||||||
|
@ -72,21 +72,21 @@ days: jours
|
|||||||
1 hour 30: 1 heure 30
|
1 hour 30: 1 heure 30
|
||||||
1 hour 45: 1 heure 45
|
1 hour 45: 1 heure 45
|
||||||
2 hours: 2 heures
|
2 hours: 2 heures
|
||||||
2 hours 30: 2 heure 30
|
2 hours 30: 2 heures 30
|
||||||
3 hours: 3 heures
|
3 hours: 3 heures
|
||||||
3 hours 30: 3 heure 30
|
3 hours 30: 3 heures 30
|
||||||
4 hours: 4 heures
|
4 hours: 4 heures
|
||||||
4 hours 30: 4 heure 30
|
4 hours 30: 4 heures 30
|
||||||
5 hours: 5 heures
|
5 hours: 5 heures
|
||||||
5 hours 30: 5 heure 30
|
5 hours 30: 5 heures 30
|
||||||
6 hours: 6 heures
|
6 hours: 6 heures
|
||||||
6 hours 30: 6 heure 30
|
6 hours 30: 6 heures 30
|
||||||
7 hours: 7 heures
|
7 hours: 7 heures
|
||||||
7 hours 30: 7 heure 30
|
7 hours 30: 7 heures 30
|
||||||
8 hours: 8 heures
|
8 hours: 8 heures
|
||||||
8 hours 30: 8 heure 30
|
8 hours 30: 8 heures 30
|
||||||
9 hours: 9 heures
|
9 hours: 9 heures
|
||||||
9 hours 30: 9 heure 30
|
9 hours 30: 9 heures 30
|
||||||
10 hours: 10 heures
|
10 hours: 10 heures
|
||||||
1/2 day: 1/2 jour
|
1/2 day: 1/2 jour
|
||||||
1 day: 1 jour
|
1 day: 1 jour
|
||||||
|
@ -440,6 +440,16 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface, HasCente
|
|||||||
return $this->startDate;
|
return $this->startDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get the date of the calendar.
|
||||||
|
*
|
||||||
|
* Useful for showing the date of the calendar event, required by twig in some places.
|
||||||
|
*/
|
||||||
|
public function getDate(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->getStartDate();
|
||||||
|
}
|
||||||
|
|
||||||
public function getStatus(): ?string
|
public function getStatus(): ?string
|
||||||
{
|
{
|
||||||
return $this->status;
|
return $this->status;
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\CalendarBundle\Menu;
|
||||||
|
|
||||||
|
use Chill\CalendarBundle\Security\Voter\CalendarVoter;
|
||||||
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Knp\Menu\MenuItem;
|
||||||
|
use Symfony\Component\Security\Core\Security;
|
||||||
|
|
||||||
|
final readonly class AccompanyingCourseQuickMenuBuilder implements LocalMenuBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private Security $security) {}
|
||||||
|
|
||||||
|
public static function getMenuIds(): array
|
||||||
|
{
|
||||||
|
return ['accompanying_course_quick_menu'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
|
{
|
||||||
|
/** @var \Chill\PersonBundle\Entity\AccompanyingPeriod $accompanyingCourse */
|
||||||
|
$accompanyingCourse = $parameters['accompanying-course'];
|
||||||
|
|
||||||
|
if ($this->security->isGranted(CalendarVoter::CREATE, $accompanyingCourse)) {
|
||||||
|
$menu
|
||||||
|
->addChild('Create a new calendar in accompanying course', [
|
||||||
|
'route' => 'chill_calendar_calendar_new',
|
||||||
|
'routeParameters' => [
|
||||||
|
'accompanying_period_id' => $accompanyingCourse->getId(),
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->setExtras([
|
||||||
|
'order' => 20,
|
||||||
|
'icon' => 'plus',
|
||||||
|
])
|
||||||
|
;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,2 @@
|
|||||||
import './scss/badge.scss';
|
import './scss/badge.scss';
|
||||||
|
import './scss/calendar-list.scss';
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
ul.calendar-list {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
& > li {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
& > li:nth-child(n+2) {
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.calendar-list {
|
||||||
|
|
||||||
|
ul.calendar-list {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > a.calendar-list__global {
|
||||||
|
display: inline-block;;
|
||||||
|
padding: 0.2rem;
|
||||||
|
min-width: 2rem;
|
||||||
|
border: 1px solid var(--bs-chill-blue);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
@ -55,7 +55,7 @@
|
|||||||
<div class="item-col">
|
<div class="item-col">
|
||||||
<ul class="list-content">
|
<ul class="list-content">
|
||||||
{% if calendar.mainUser is not empty %}
|
{% if calendar.mainUser is not empty %}
|
||||||
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ calendar.mainUser|chill_entity_render_box({'at_date': calendar.startDate}) }}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@ -132,7 +132,7 @@
|
|||||||
<li class="cancel">
|
<li class="cancel">
|
||||||
<span class="createdBy">
|
<span class="createdBy">
|
||||||
{{ 'Created by'|trans }}
|
{{ 'Created by'|trans }}
|
||||||
<b>{{ calendar.activity.createdBy|chill_entity_render_string }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
<b>{{ calendar.activity.createdBy|chill_entity_render_string({'at_date': calendar.activity.createdAt}) }}</b>, {{ 'on'|trans }} {{ calendar.activity.createdAt|format_datetime('short', 'short') }}
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
{% if is_granted('CHILL_ACTIVITY_SEE', calendar.activity) %}
|
||||||
|
@ -89,7 +89,7 @@ class CalendarVoter extends AbstractChillVoter implements ProvideRoleHierarchyIn
|
|||||||
switch ($attribute) {
|
switch ($attribute) {
|
||||||
case self::SEE:
|
case self::SEE:
|
||||||
case self::CREATE:
|
case self::CREATE:
|
||||||
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep()) {
|
if (AccompanyingPeriod::STEP_DRAFT === $subject->getStep() || AccompanyingPeriod::STEP_CLOSED === $subject->getStep()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,6 +26,7 @@ The calendar item has been successfully removed.: Le rendez-vous a été supprim
|
|||||||
From the day: Du
|
From the day: Du
|
||||||
to the day: au
|
to the day: au
|
||||||
Transform to activity: Transformer en échange
|
Transform to activity: Transformer en échange
|
||||||
|
Create a new calendar in accompanying course: Créer un rendez-vous dans le parcours
|
||||||
Will send SMS: Un SMS de rappel sera envoyé
|
Will send SMS: Un SMS de rappel sera envoyé
|
||||||
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
Will not send SMS: Aucun SMS de rappel ne sera envoyé
|
||||||
SMS already sent: Un SMS a été envoyé
|
SMS already sent: Un SMS a été envoyé
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocGeneratorBundle\Test;
|
||||||
|
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T of object
|
||||||
|
*/
|
||||||
|
abstract class DocGenNormalizerTestAbstract extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testNullValueHasSameKeysAsNull(): void
|
||||||
|
{
|
||||||
|
$normalizedObject = $this->getNormalizer()->normalize($this->provideNotNullObject(), 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
|
||||||
|
]);
|
||||||
|
$nullNormalizedObject = $this->getNormalizer()->normalize(null, 'docgen', [
|
||||||
|
AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => $this->provideDocGenExpectClass(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertEqualsCanonicalizing(array_keys($normalizedObject), array_keys($nullNormalizedObject));
|
||||||
|
self::assertArrayHasKey('isNull', $nullNormalizedObject, 'each object must have an "isNull" key');
|
||||||
|
self::assertTrue($nullNormalizedObject['isNull'], 'isNull key must be true for null objects');
|
||||||
|
self::assertFalse($normalizedObject['isNull'], 'isNull key must be false for null objects');
|
||||||
|
|
||||||
|
foreach ($normalizedObject as $key => $value) {
|
||||||
|
if (in_array($key, ['isNull', 'type'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
if (array_is_list($value)) {
|
||||||
|
self::assertEquals([], $nullNormalizedObject[$key], "list must be serialized as an empty array, in {$key}");
|
||||||
|
} else {
|
||||||
|
self::assertEqualsCanonicalizing(array_keys($value), array_keys($nullNormalizedObject[$key]), "sub-object must have the same keys, in {$key}");
|
||||||
|
}
|
||||||
|
} elseif (is_string($value)) {
|
||||||
|
self::assertEquals('', $nullNormalizedObject[$key], 'strings must be ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return T
|
||||||
|
*/
|
||||||
|
abstract public function provideNotNullObject(): object;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return class-string<T>
|
||||||
|
*/
|
||||||
|
abstract public function provideDocGenExpectClass(): string;
|
||||||
|
|
||||||
|
abstract public function getNormalizer(): NormalizerInterface;
|
||||||
|
}
|
@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Controller;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
|
use Chill\DocStoreBundle\Service\StoredObjectManagerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
class SignatureRequestController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private MessageBusInterface $messageBus,
|
||||||
|
private StoredObjectManagerInterface $storedObjectManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Route('/api/1.0/document/workflow/{id}/signature-request', name: 'chill_docstore_signature_request')]
|
||||||
|
public function processSignature(StoredObject $storedObject): Response
|
||||||
|
{
|
||||||
|
$content = $this->storedObjectManager->read($storedObject);
|
||||||
|
|
||||||
|
$this->messageBus->dispatch(new RequestPdfSignMessage(
|
||||||
|
0,
|
||||||
|
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||||
|
0,
|
||||||
|
'test signature',
|
||||||
|
'Mme Caroline Diallo',
|
||||||
|
$content
|
||||||
|
));
|
||||||
|
|
||||||
|
return new Response('<html><head><title>test</title></head><body><p>ok</p></body></html>');
|
||||||
|
}
|
||||||
|
}
|
@ -313,4 +313,19 @@ class StoredObject implements Document, TrackCreationInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function saveHistory(): void
|
||||||
|
{
|
||||||
|
if ('' === $this->getFilename()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->datas['history'][] = [
|
||||||
|
'filename' => $this->getFilename(),
|
||||||
|
'iv' => $this->getIv(),
|
||||||
|
'key_infos' => $this->getKeyInfos(),
|
||||||
|
'type' => $this->getType(),
|
||||||
|
'before' => (new \DateTimeImmutable('now'))->getTimestamp(),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,8 +57,8 @@ class StoredObjectDataMapper implements DataMapperInterface
|
|||||||
|
|
||||||
/** @var StoredObject $viewData */
|
/** @var StoredObject $viewData */
|
||||||
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
|
if ($viewData->getFilename() !== $forms['stored_object']->getData()['filename']) {
|
||||||
// we do not want to erase the previous object
|
// we want to keep the previous history
|
||||||
$viewData = new StoredObject();
|
$viewData->saveHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
$viewData->setFilename($forms['stored_object']->getData()['filename']);
|
$viewData->setFilename($forms['stored_object']->getData()['filename']);
|
||||||
|
@ -4,13 +4,13 @@
|
|||||||
Actions
|
Actions
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type)">
|
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.storedObject.status !== 'stored_object_created'">
|
||||||
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
<wopi-edit-button :stored-object="props.storedObject" :classes="{'dropdown-item': true}" :execute-before-leave="props.executeBeforeLeave"></wopi-edit-button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
|
<li v-if="props.canEdit && is_extension_editable(props.storedObject.type) && props.davLink !== undefined && props.davLinkExpiration !== undefined">
|
||||||
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
|
<desktop-edit-button :classes="{'dropdown-item': true}" :edit-link="props.davLink" :expiration-link="props.davLinkExpiration"></desktop-edit-button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf">
|
<li v-if="props.storedObject.type != 'application/pdf' && is_extension_viewable(props.storedObject.type) && props.canConvertPdf && props.storedObject.status !== 'stored_object_created'">
|
||||||
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
<convert-button :stored-object="props.storedObject" :filename="filename" :classes="{'dropdown-item': true}"></convert-button>
|
||||||
</li>
|
</li>
|
||||||
<li v-if="props.canDownload">
|
<li v-if="props.canDownload">
|
||||||
|
@ -13,7 +13,7 @@ import {reactive} from "vue";
|
|||||||
import {StoredObject, StoredObjectCreated} from "../../types";
|
import {StoredObject, StoredObjectCreated} from "../../types";
|
||||||
|
|
||||||
interface ConvertButtonConfig {
|
interface ConvertButtonConfig {
|
||||||
storedObject: StoredObject|StoredObjectCreated,
|
storedObject: StoredObject,
|
||||||
classes: { [key: string]: boolean},
|
classes: { [key: string]: boolean},
|
||||||
filename?: string,
|
filename?: string,
|
||||||
};
|
};
|
||||||
|
@ -11,7 +11,7 @@ import {build_wopi_editor_link} from "./helpers";
|
|||||||
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
import {StoredObject, StoredObjectCreated, WopiEditButtonExecutableBeforeLeaveFunction} from "../../types";
|
||||||
|
|
||||||
interface WopiEditButtonConfig {
|
interface WopiEditButtonConfig {
|
||||||
storedObject: StoredObject|StoredObjectCreated,
|
storedObject: StoredObject,
|
||||||
returnPath?: string,
|
returnPath?: string,
|
||||||
classes: {[k: string] : boolean},
|
classes: {[k: string] : boolean},
|
||||||
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
executeBeforeLeave?: WopiEditButtonExecutableBeforeLeaveFunction,
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message which is received when a pdf is signed.
|
||||||
|
*/
|
||||||
|
final readonly class PdfSignedMessage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public readonly int $signatureId,
|
||||||
|
public readonly string $content
|
||||||
|
) {}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
|
final readonly class PdfSignedMessageHandler implements MessageHandlerInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* log prefix.
|
||||||
|
*/
|
||||||
|
private const P = '[pdf signed message] ';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private LoggerInterface $logger,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function __invoke(PdfSignedMessage $message): void
|
||||||
|
{
|
||||||
|
$this->logger->info(self::P.'a message is received', ['signaturedId' => $message->signatureId]);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||||
|
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode (and requeue) @see{PdfSignedMessage}, which comes from an external producer.
|
||||||
|
*/
|
||||||
|
final readonly class PdfSignedMessageSerializer implements SerializerInterface
|
||||||
|
{
|
||||||
|
public function decode(array $encodedEnvelope): Envelope
|
||||||
|
{
|
||||||
|
$body = $encodedEnvelope['body'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
$decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
|
||||||
|
} catch (\JsonException $e) {
|
||||||
|
throw new MessageDecodingFailedException('Could not deserialize message', previous: $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('signatureId', $decoded) || !array_key_exists('content', $decoded)) {
|
||||||
|
throw new MessageDecodingFailedException('Could not find expected keys: signatureId or content');
|
||||||
|
}
|
||||||
|
|
||||||
|
$content = base64_decode($decoded['content'], true);
|
||||||
|
|
||||||
|
if (false === $content) {
|
||||||
|
throw new MessageDecodingFailedException('Invalid character found in the base64 encoded content');
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = new PdfSignedMessage($decoded['signatureId'], $content);
|
||||||
|
|
||||||
|
return new Envelope($message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encode(Envelope $envelope): array
|
||||||
|
{
|
||||||
|
$message = $envelope->getMessage();
|
||||||
|
|
||||||
|
if (!$message instanceof PdfSignedMessage) {
|
||||||
|
throw new MessageDecodingFailedException('Expected a PdfSignedMessage');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'signatureId' => $message->signatureId,
|
||||||
|
'content' => base64_encode($message->content),
|
||||||
|
];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => json_encode($data, JSON_THROW_ON_ERROR),
|
||||||
|
'headers' => [],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message which is sent when we request a signature on a pdf.
|
||||||
|
*/
|
||||||
|
final readonly class RequestPdfSignMessage
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public int $signatureId,
|
||||||
|
public PDFSignatureZone $PDFSignatureZone,
|
||||||
|
public int $signatureZoneIndex,
|
||||||
|
public string $reason,
|
||||||
|
public string $signerText,
|
||||||
|
public string $content,
|
||||||
|
) {}
|
||||||
|
}
|
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Messenger\Exception\MessageDecodingFailedException;
|
||||||
|
use Symfony\Component\Messenger\Stamp\NonSendableStampInterface;
|
||||||
|
use Symfony\Component\Messenger\Transport\Serialization\SerializerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Serialize a RequestPdfSignMessage, for external consumer.
|
||||||
|
*/
|
||||||
|
final readonly class RequestPdfSignMessageSerializer implements SerializerInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private NormalizerInterface $normalizer,
|
||||||
|
private DenormalizerInterface $denormalizer,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function decode(array $encodedEnvelope): Envelope
|
||||||
|
{
|
||||||
|
$body = $encodedEnvelope['body'];
|
||||||
|
$headers = $encodedEnvelope['headers'];
|
||||||
|
|
||||||
|
if (RequestPdfSignMessage::class !== ($headers['Message'] ?? null)) {
|
||||||
|
throw new MessageDecodingFailedException('serializer does not support this message');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = json_decode($body, true);
|
||||||
|
|
||||||
|
$zoneSignature = $this->denormalizer->denormalize($data['signatureZone'], PDFSignatureZone::class, 'json', [
|
||||||
|
AbstractNormalizer::GROUPS => ['write'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$content = base64_decode($data['content'], true);
|
||||||
|
|
||||||
|
if (false === $content) {
|
||||||
|
throw new MessageDecodingFailedException('the content could not be converted from base64 encoding');
|
||||||
|
}
|
||||||
|
|
||||||
|
$message = new RequestPdfSignMessage(
|
||||||
|
$data['signatureId'],
|
||||||
|
$zoneSignature,
|
||||||
|
$data['signatureZoneIndex'],
|
||||||
|
$data['reason'],
|
||||||
|
$data['signerText'],
|
||||||
|
$content,
|
||||||
|
);
|
||||||
|
|
||||||
|
// in case of redelivery, unserialize any stamps
|
||||||
|
$stamps = [];
|
||||||
|
if (isset($headers['stamps'])) {
|
||||||
|
$stamps = unserialize($headers['stamps']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Envelope($message, $stamps);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function encode(Envelope $envelope): array
|
||||||
|
{
|
||||||
|
$message = $envelope->getMessage();
|
||||||
|
|
||||||
|
if (!$message instanceof RequestPdfSignMessage) {
|
||||||
|
throw new MessageDecodingFailedException('Message is not a RequestPdfSignMessage');
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'signatureId' => $message->signatureId,
|
||||||
|
'signatureZoneIndex' => $message->signatureZoneIndex,
|
||||||
|
'signatureZone' => $this->normalizer->normalize($message->PDFSignatureZone, 'json', [AbstractNormalizer::GROUPS => ['read']]),
|
||||||
|
'reason' => $message->reason,
|
||||||
|
'signerText' => $message->signerText,
|
||||||
|
'content' => base64_encode($message->content),
|
||||||
|
];
|
||||||
|
|
||||||
|
$allStamps = [];
|
||||||
|
foreach ($envelope->all() as $stamp) {
|
||||||
|
if ($stamp instanceof NonSendableStampInterface) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$allStamps = [...$allStamps, ...$stamp];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'body' => json_encode($data, JSON_THROW_ON_ERROR, 512),
|
||||||
|
'headers' => [
|
||||||
|
'stamps' => serialize($allStamps),
|
||||||
|
'Message' => RequestPdfSignMessage::class,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Tests\Entity;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Entity\StoredObject;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class StoredObjectTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
public function testSaveHistory(): void
|
||||||
|
{
|
||||||
|
$storedObject = new StoredObject();
|
||||||
|
$storedObject
|
||||||
|
->setFilename('test_0')
|
||||||
|
->setIv([2, 4, 6, 8])
|
||||||
|
->setKeyInfos(['key' => ['data0' => 'data0']])
|
||||||
|
->setType('text/html');
|
||||||
|
|
||||||
|
$storedObject->saveHistory();
|
||||||
|
|
||||||
|
$storedObject
|
||||||
|
->setFilename('test_1')
|
||||||
|
->setIv([8, 10, 12])
|
||||||
|
->setKeyInfos(['key' => ['data1' => 'data1']])
|
||||||
|
->setType('text/text');
|
||||||
|
|
||||||
|
$storedObject->saveHistory();
|
||||||
|
|
||||||
|
self::assertEquals('test_0', $storedObject->getDatas()['history'][0]['filename']);
|
||||||
|
self::assertEquals([2, 4, 6, 8], $storedObject->getDatas()['history'][0]['iv']);
|
||||||
|
self::assertEquals(['key' => ['data0' => 'data0']], $storedObject->getDatas()['history'][0]['key_infos']);
|
||||||
|
self::assertEquals('text/html', $storedObject->getDatas()['history'][0]['type']);
|
||||||
|
|
||||||
|
self::assertEquals('test_1', $storedObject->getDatas()['history'][1]['filename']);
|
||||||
|
self::assertEquals([8, 10, 12], $storedObject->getDatas()['history'][1]['iv']);
|
||||||
|
self::assertEquals(['key' => ['data1' => 'data1']], $storedObject->getDatas()['history'][1]['key_infos']);
|
||||||
|
self::assertEquals('text/text', $storedObject->getDatas()['history'][1]['type']);
|
||||||
|
}
|
||||||
|
}
|
@ -56,14 +56,14 @@ class StoredObjectTypeTest extends TypeTestCase
|
|||||||
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
|
{"filename":"abcdef","iv":[10, 15, 20, 30],"keyInfos":[],"type":"text/html","status":"object_store_created"}
|
||||||
JSON];
|
JSON];
|
||||||
$model = new StoredObject();
|
$model = new StoredObject();
|
||||||
$originalObjectId = spl_object_id($model);
|
$originalObjectId = spl_object_hash($model);
|
||||||
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
$form = $this->factory->create(StoredObjectType::class, $model, ['has_title' => true]);
|
||||||
|
|
||||||
$form->submit($formData);
|
$form->submit($formData);
|
||||||
|
|
||||||
$this->assertTrue($form->isSynchronized());
|
$this->assertTrue($form->isSynchronized());
|
||||||
$model = $form->getData();
|
$model = $form->getData();
|
||||||
$this->assertNotEquals($originalObjectId, spl_object_hash($model));
|
$this->assertEquals($originalObjectId, spl_object_hash($model));
|
||||||
$this->assertEquals('abcdef', $model->getFilename());
|
$this->assertEquals('abcdef', $model->getFilename());
|
||||||
$this->assertEquals([10, 15, 20, 30], $model->getIv());
|
$this->assertEquals([10, 15, 20, 30], $model->getIv());
|
||||||
$this->assertEquals('text/html', $model->getType());
|
$this->assertEquals('text/html', $model->getType());
|
||||||
|
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessage;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\PdfSignedMessageSerializer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class PdfSignedMessageSerializerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testDecode(): void
|
||||||
|
{
|
||||||
|
$asString = <<<'JSON'
|
||||||
|
{"signatureId": 0, "content": "dGVzdAo="}
|
||||||
|
JSON;
|
||||||
|
|
||||||
|
$actual = $this->buildSerializer()->decode(['body' => $asString]);
|
||||||
|
|
||||||
|
self::assertInstanceOf(Envelope::class, $actual);
|
||||||
|
$message = $actual->getMessage();
|
||||||
|
self::assertInstanceOf(PdfSignedMessage::class, $message);
|
||||||
|
self::assertEquals("test\n", $message->content);
|
||||||
|
self::assertEquals(0, $message->signatureId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEncode(): void
|
||||||
|
{
|
||||||
|
$envelope = new Envelope(
|
||||||
|
new PdfSignedMessage(0, "test\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $this->buildSerializer()->encode($envelope);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
self::assertArrayHasKey('body', $actual);
|
||||||
|
self::assertArrayHasKey('headers', $actual);
|
||||||
|
self::assertEquals([], $actual['headers']);
|
||||||
|
|
||||||
|
self::assertEquals(<<<'JSON'
|
||||||
|
{"signatureId":0,"content":"dGVzdAo="}
|
||||||
|
JSON, $actual['body']);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSerializer(): PdfSignedMessageSerializer
|
||||||
|
{
|
||||||
|
return new PdfSignedMessageSerializer();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\DocStoreBundle\Tests\Service\Signature\Driver\BaseSigner;
|
||||||
|
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFPage;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\PDFSignatureZone;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessage;
|
||||||
|
use Chill\DocStoreBundle\Service\Signature\Driver\BaseSigner\RequestPdfSignMessageSerializer;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Messenger\Envelope;
|
||||||
|
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
use Symfony\Component\Serializer\Serializer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RequestPdfSignMessageSerializerTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testEncode(): void
|
||||||
|
{
|
||||||
|
$serializer = $this->buildSerializer();
|
||||||
|
|
||||||
|
$envelope = new Envelope(
|
||||||
|
$request = new RequestPdfSignMessage(
|
||||||
|
0,
|
||||||
|
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||||
|
0,
|
||||||
|
'metadata to add to the signature',
|
||||||
|
'Mme Caroline Diallo',
|
||||||
|
'abc'
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $serializer->encode($envelope);
|
||||||
|
$expectedBody = json_encode([
|
||||||
|
'signatureId' => $request->signatureId,
|
||||||
|
'signatureZoneIndex' => $request->signatureZoneIndex,
|
||||||
|
'signatureZone' => ['x' => 10.0],
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'signerText' => $request->signerText,
|
||||||
|
'content' => base64_encode($request->content),
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertIsArray($actual);
|
||||||
|
self::assertArrayHasKey('body', $actual);
|
||||||
|
self::assertArrayHasKey('headers', $actual);
|
||||||
|
self::assertEquals($expectedBody, $actual['body']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDecode(): void
|
||||||
|
{
|
||||||
|
$serializer = $this->buildSerializer();
|
||||||
|
|
||||||
|
$request = new RequestPdfSignMessage(
|
||||||
|
0,
|
||||||
|
new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0)),
|
||||||
|
0,
|
||||||
|
'metadata to add to the signature',
|
||||||
|
'Mme Caroline Diallo',
|
||||||
|
'abc'
|
||||||
|
);
|
||||||
|
|
||||||
|
$bodyAsString = json_encode([
|
||||||
|
'signatureId' => $request->signatureId,
|
||||||
|
'signatureZoneIndex' => $request->signatureZoneIndex,
|
||||||
|
'signatureZone' => ['x' => 10.0],
|
||||||
|
'reason' => $request->reason,
|
||||||
|
'signerText' => $request->signerText,
|
||||||
|
'content' => base64_encode($request->content),
|
||||||
|
], JSON_THROW_ON_ERROR);
|
||||||
|
|
||||||
|
$actual = $serializer->decode([
|
||||||
|
'body' => $bodyAsString,
|
||||||
|
'headers' => [
|
||||||
|
'Message' => RequestPdfSignMessage::class,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertInstanceOf(RequestPdfSignMessage::class, $actual->getMessage());
|
||||||
|
self::assertEquals($request->signatureId, $actual->getMessage()->signatureId);
|
||||||
|
self::assertEquals($request->signatureZoneIndex, $actual->getMessage()->signatureZoneIndex);
|
||||||
|
self::assertEquals($request->reason, $actual->getMessage()->reason);
|
||||||
|
self::assertEquals($request->signerText, $actual->getMessage()->signerText);
|
||||||
|
self::assertEquals($request->content, $actual->getMessage()->content);
|
||||||
|
self::assertNotNull($actual->getMessage()->PDFSignatureZone);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildSerializer(): RequestPdfSignMessageSerializer
|
||||||
|
{
|
||||||
|
$normalizer =
|
||||||
|
new class () implements NormalizerInterface {
|
||||||
|
public function normalize($object, ?string $format = null, array $context = []): array
|
||||||
|
{
|
||||||
|
if (!$object instanceof PDFSignatureZone) {
|
||||||
|
throw new UnexpectedValueException('expected RequestPdfSignMessage');
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'x' => $object->x,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsNormalization($data, ?string $format = null): bool
|
||||||
|
{
|
||||||
|
return $data instanceof PDFSignatureZone;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$denormalizer = new class () implements DenormalizerInterface {
|
||||||
|
public function denormalize($data, string $type, ?string $format = null, array $context = [])
|
||||||
|
{
|
||||||
|
return new PDFSignatureZone(10.0, 10.0, 180.0, 180.0, new PDFPage(0, 500.0, 800.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supportsDenormalization($data, string $type, ?string $format = null)
|
||||||
|
{
|
||||||
|
return PDFSignatureZone::class === $type;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$serializer = new Serializer([$normalizer, $denormalizer]);
|
||||||
|
|
||||||
|
return new RequestPdfSignMessageSerializer($serializer, $serializer);
|
||||||
|
}
|
||||||
|
}
|
@ -632,7 +632,7 @@ class ExportController extends AbstractController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function rebuildRawData(string $key): array
|
private function rebuildRawData(?string $key): array
|
||||||
{
|
{
|
||||||
if (null === $key) {
|
if (null === $key) {
|
||||||
throw $this->createNotFoundException('key does not exists');
|
throw $this->createNotFoundException('key does not exists');
|
||||||
|
@ -61,8 +61,6 @@ final class PermissionsGroupController extends AbstractController
|
|||||||
$form = $this->createAddRoleScopeForm($permissionsGroup);
|
$form = $this->createAddRoleScopeForm($permissionsGroup);
|
||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
|
|
||||||
dump($form->isSubmitted());
|
|
||||||
|
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$roleScope = $this->getPersistentRoleScopeBy(
|
$roleScope = $this->getPersistentRoleScopeBy(
|
||||||
$form['composed_role_scope']->getData()->getRole(),
|
$form['composed_role_scope']->getData()->getRole(),
|
||||||
|
@ -22,6 +22,7 @@ use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
|
|||||||
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
use Chill\MainBundle\Security\Authorization\EntityWorkflowVoter;
|
||||||
use Chill\MainBundle\Security\ChillSecurity;
|
use Chill\MainBundle\Security\ChillSecurity;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
@ -279,7 +280,7 @@ class WorkflowController extends AbstractController
|
|||||||
|
|
||||||
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
if (\count($workflow->getEnabledTransitions($entityWorkflow)) > 0) {
|
||||||
// possible transition
|
// possible transition
|
||||||
|
$stepDTO = new WorkflowTransitionContextDTO($entityWorkflow);
|
||||||
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
$usersInvolved = $entityWorkflow->getUsersInvolved();
|
||||||
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
$currentUserFound = array_search($this->security->getUser(), $usersInvolved, true);
|
||||||
|
|
||||||
@ -289,9 +290,8 @@ class WorkflowController extends AbstractController
|
|||||||
|
|
||||||
$transitionForm = $this->createForm(
|
$transitionForm = $this->createForm(
|
||||||
WorkflowStepType::class,
|
WorkflowStepType::class,
|
||||||
$entityWorkflow->getCurrentStep(),
|
$stepDTO,
|
||||||
[
|
[
|
||||||
'transition' => true,
|
|
||||||
'entity_workflow' => $entityWorkflow,
|
'entity_workflow' => $entityWorkflow,
|
||||||
'suggested_users' => $usersInvolved,
|
'suggested_users' => $usersInvolved,
|
||||||
]
|
]
|
||||||
@ -310,12 +310,7 @@ class WorkflowController extends AbstractController
|
|||||||
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
throw $this->createAccessDeniedException(sprintf("not allowed to apply transition {$transition}: %s", implode(', ', $msgs)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO symfony 5: add those "future" on context ($workflow->apply($entityWorkflow, $transition, $context)
|
$workflow->apply($entityWorkflow, $transition, ['context' => $stepDTO]);
|
||||||
$entityWorkflow->futureCcUsers = $transitionForm['future_cc_users']->getData() ?? [];
|
|
||||||
$entityWorkflow->futureDestUsers = $transitionForm['future_dest_users']->getData() ?? [];
|
|
||||||
$entityWorkflow->futureDestEmails = $transitionForm['future_dest_emails']->getData() ?? [];
|
|
||||||
|
|
||||||
$workflow->apply($entityWorkflow, $transition);
|
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
|
@ -216,13 +216,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->mainLocation;
|
return $this->mainLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getMainScope(?\DateTimeImmutable $at = null): ?Scope
|
public function getMainScope(?\DateTimeImmutable $atDate = null): ?Scope
|
||||||
{
|
{
|
||||||
$at ??= new \DateTimeImmutable('now');
|
$atDate ??= new \DateTimeImmutable('now');
|
||||||
|
|
||||||
foreach ($this->scopeHistories as $scopeHistory) {
|
foreach ($this->scopeHistories as $scopeHistory) {
|
||||||
if ($at >= $scopeHistory->getStartDate() && (
|
if ($atDate >= $scopeHistory->getStartDate() && (
|
||||||
null === $scopeHistory->getEndDate() || $at < $scopeHistory->getEndDate()
|
null === $scopeHistory->getEndDate() || $atDate < $scopeHistory->getEndDate()
|
||||||
)) {
|
)) {
|
||||||
return $scopeHistory->getScope();
|
return $scopeHistory->getScope();
|
||||||
}
|
}
|
||||||
@ -265,13 +265,13 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->salt;
|
return $this->salt;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getUserJob(?\DateTimeImmutable $at = null): ?UserJob
|
public function getUserJob(?\DateTimeImmutable $atDate = null): ?UserJob
|
||||||
{
|
{
|
||||||
$at ??= new \DateTimeImmutable('now');
|
$atDate ??= new \DateTimeImmutable('now');
|
||||||
|
|
||||||
foreach ($this->jobHistories as $jobHistory) {
|
foreach ($this->jobHistories as $jobHistory) {
|
||||||
if ($at >= $jobHistory->getStartDate() && (
|
if ($atDate >= $jobHistory->getStartDate() && (
|
||||||
null === $jobHistory->getEndDate() || $at < $jobHistory->getEndDate()
|
null === $jobHistory->getEndDate() || $atDate < $jobHistory->getEndDate()
|
||||||
)) {
|
)) {
|
||||||
return $jobHistory->getJob();
|
return $jobHistory->getJob();
|
||||||
}
|
}
|
||||||
@ -285,6 +285,11 @@ class User implements UserInterface, \Stringable, PasswordAuthenticatedUserInter
|
|||||||
return $this->jobHistories;
|
return $this->jobHistories;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUserScopeHistories(): Collection
|
||||||
|
{
|
||||||
|
return $this->scopeHistories;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return ArrayCollection|UserJobHistory[]
|
* @return ArrayCollection|UserJobHistory[]
|
||||||
*/
|
*/
|
||||||
|
@ -17,9 +17,9 @@ use Chill\MainBundle\Doctrine\Model\TrackUpdateInterface;
|
|||||||
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
use Chill\MainBundle\Doctrine\Model\TrackUpdateTrait;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
use Chill\MainBundle\Workflow\Validator\EntityWorkflowCreation;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\Common\Collections\Order;
|
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Serializer\Annotation as Serializer;
|
use Symfony\Component\Serializer\Annotation as Serializer;
|
||||||
use Symfony\Component\Validator\Constraints as Assert;
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
@ -34,35 +34,6 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
|
|
||||||
use TrackUpdateTrait;
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
/**
|
|
||||||
* a list of future cc users for the next steps.
|
|
||||||
*
|
|
||||||
* @var array|User[]
|
|
||||||
*/
|
|
||||||
public array $futureCcUsers = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* a list of future dest emails for the next steps.
|
|
||||||
*
|
|
||||||
* This is in used in order to let controller inform who will be the future emails which will validate
|
|
||||||
* the next step. This is necessary to perform some computation about the next emails, before they are
|
|
||||||
* associated to the entity EntityWorkflowStep.
|
|
||||||
*
|
|
||||||
* @var array|string[]
|
|
||||||
*/
|
|
||||||
public array $futureDestEmails = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* a list of future dest users for the next steps.
|
|
||||||
*
|
|
||||||
* This is in used in order to let controller inform who will be the future users which will validate
|
|
||||||
* the next step. This is necessary to perform some computation about the next users, before they are
|
|
||||||
* associated to the entity EntityWorkflowStep.
|
|
||||||
*
|
|
||||||
* @var array|User[]
|
|
||||||
*/
|
|
||||||
public array $futureDestUsers = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<EntityWorkflowComment>
|
* @var Collection<EntityWorkflowComment>
|
||||||
*/
|
*/
|
||||||
@ -442,11 +413,23 @@ class EntityWorkflow implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
*
|
*
|
||||||
* @return $this
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function setStep(string $step): self
|
public function setStep(string $step, WorkflowTransitionContextDTO $transitionContextDTO): self
|
||||||
{
|
{
|
||||||
$newStep = new EntityWorkflowStep();
|
$newStep = new EntityWorkflowStep();
|
||||||
$newStep->setCurrentStep($step);
|
$newStep->setCurrentStep($step);
|
||||||
|
|
||||||
|
foreach ($transitionContextDTO->futureCcUsers as $user) {
|
||||||
|
$newStep->addCcUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($transitionContextDTO->futureDestUsers as $user) {
|
||||||
|
$newStep->addDestUser($user);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($transitionContextDTO->futureDestEmails as $email) {
|
||||||
|
$newStep->addDestEmail($email);
|
||||||
|
}
|
||||||
|
|
||||||
// copy the freeze
|
// copy the freeze
|
||||||
if ($this->isFreeze()) {
|
if ($this->isFreeze()) {
|
||||||
$newStep->setFreezeAfter(true);
|
$newStep->setFreezeAfter(true);
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Entity\Workflow;
|
||||||
|
|
||||||
|
enum EntityWorkflowSignatureStateEnum: string
|
||||||
|
{
|
||||||
|
case PENDING = 'pending';
|
||||||
|
case SIGNED = 'signed';
|
||||||
|
case REJECTED = 'rejected';
|
||||||
|
case CANCELED = 'canceled';
|
||||||
|
}
|
@ -42,19 +42,25 @@ class EntityWorkflowStep
|
|||||||
private array $destEmail = [];
|
private array $destEmail = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<User>
|
* @var Collection<int, User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user')]
|
||||||
private Collection $destUser;
|
private Collection $destUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var Collection<User>
|
* @var Collection<int, User>
|
||||||
*/
|
*/
|
||||||
#[ORM\ManyToMany(targetEntity: User::class)]
|
#[ORM\ManyToMany(targetEntity: User::class)]
|
||||||
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
#[ORM\JoinTable(name: 'chill_main_workflow_entity_step_user_by_accesskey')]
|
||||||
private Collection $destUserByAccessKey;
|
private Collection $destUserByAccessKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Collection <int, EntityWorkflowStepSignature>
|
||||||
|
*/
|
||||||
|
#[ORM\OneToMany(mappedBy: 'step', targetEntity: EntityWorkflowStepSignature::class, cascade: ['persist'], orphanRemoval: true)]
|
||||||
|
private Collection $signatures;
|
||||||
|
|
||||||
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
#[ORM\ManyToOne(targetEntity: EntityWorkflow::class, inversedBy: 'steps')]
|
||||||
private ?EntityWorkflow $entityWorkflow = null;
|
private ?EntityWorkflow $entityWorkflow = null;
|
||||||
|
|
||||||
@ -97,6 +103,7 @@ class EntityWorkflowStep
|
|||||||
$this->ccUser = new ArrayCollection();
|
$this->ccUser = new ArrayCollection();
|
||||||
$this->destUser = new ArrayCollection();
|
$this->destUser = new ArrayCollection();
|
||||||
$this->destUserByAccessKey = new ArrayCollection();
|
$this->destUserByAccessKey = new ArrayCollection();
|
||||||
|
$this->signatures = new ArrayCollection();
|
||||||
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
$this->accessKey = bin2hex(openssl_random_pseudo_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -136,6 +143,29 @@ class EntityWorkflowStep
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal use @see{EntityWorkflowStepSignature}'s constructor instead
|
||||||
|
*/
|
||||||
|
public function addSignature(EntityWorkflowStepSignature $signature): self
|
||||||
|
{
|
||||||
|
if (!$this->signatures->contains($signature)) {
|
||||||
|
$this->signatures[] = $signature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function removeSignature(EntityWorkflowStepSignature $signature): self
|
||||||
|
{
|
||||||
|
if ($this->signatures->contains($signature)) {
|
||||||
|
$this->signatures->removeElement($signature);
|
||||||
|
}
|
||||||
|
|
||||||
|
$signature->detachEntityWorkflowStep();
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
public function getAccessKey(): string
|
public function getAccessKey(): string
|
||||||
{
|
{
|
||||||
return $this->accessKey;
|
return $this->accessKey;
|
||||||
@ -198,6 +228,14 @@ class EntityWorkflowStep
|
|||||||
return $this->entityWorkflow;
|
return $this->entityWorkflow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, EntityWorkflowStepSignature>
|
||||||
|
*/
|
||||||
|
public function getSignatures(): Collection
|
||||||
|
{
|
||||||
|
return $this->signatures;
|
||||||
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
|
@ -0,0 +1,156 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity]
|
||||||
|
#[ORM\Table(name: 'chill_main_workflow_entity_step_signature')]
|
||||||
|
class EntityWorkflowStepSignature implements TrackCreationInterface, TrackUpdateInterface
|
||||||
|
{
|
||||||
|
use TrackCreationTrait;
|
||||||
|
use TrackUpdateTrait;
|
||||||
|
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, unique: true)]
|
||||||
|
#[ORM\GeneratedValue(strategy: 'AUTO')]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: User::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?User $userSigner = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: Person::class)]
|
||||||
|
#[ORM\JoinColumn(nullable: true)]
|
||||||
|
private ?Person $personSigner = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::STRING, length: 50, nullable: false, enumType: EntityWorkflowSignatureStateEnum::class)]
|
||||||
|
private EntityWorkflowSignatureStateEnum $state = EntityWorkflowSignatureStateEnum::PENDING;
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::DATETIMETZ_IMMUTABLE, nullable: true, options: ['default' => null])]
|
||||||
|
private ?\DateTimeImmutable $stateDate = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::JSON, nullable: false, options: ['default' => '[]'])]
|
||||||
|
private array $signatureMetadata = [];
|
||||||
|
|
||||||
|
#[ORM\Column(type: \Doctrine\DBAL\Types\Types::INTEGER, nullable: true, options: ['default' => null])]
|
||||||
|
private ?int $zoneSignatureIndex = null;
|
||||||
|
|
||||||
|
#[ORM\ManyToOne(targetEntity: EntityWorkflowStep::class, inversedBy: 'signatures')]
|
||||||
|
#[ORM\JoinColumn(nullable: false)]
|
||||||
|
private ?EntityWorkflowStep $step = null;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
EntityWorkflowStep $step,
|
||||||
|
User|Person $signer,
|
||||||
|
) {
|
||||||
|
$this->step = $step;
|
||||||
|
$step->addSignature($this);
|
||||||
|
$this->setSigner($signer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function setSigner(User|Person $signer): void
|
||||||
|
{
|
||||||
|
if ($signer instanceof User) {
|
||||||
|
$this->userSigner = $signer;
|
||||||
|
} else {
|
||||||
|
$this->personSigner = $signer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStep(): EntityWorkflowStep
|
||||||
|
{
|
||||||
|
return $this->step;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSigner(): User|Person
|
||||||
|
{
|
||||||
|
if (null !== $this->userSigner) {
|
||||||
|
return $this->userSigner;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->personSigner;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSignatureMetadata(): array
|
||||||
|
{
|
||||||
|
return $this->signatureMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setSignatureMetadata(array $signatureMetadata): EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
$this->signatureMetadata = $signatureMetadata;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getState(): EntityWorkflowSignatureStateEnum
|
||||||
|
{
|
||||||
|
return $this->state;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setState(EntityWorkflowSignatureStateEnum $state): EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
$this->state = $state;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getStateDate(): ?\DateTimeImmutable
|
||||||
|
{
|
||||||
|
return $this->stateDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setStateDate(?\DateTimeImmutable $stateDate): EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
$this->stateDate = $stateDate;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getZoneSignatureIndex(): ?int
|
||||||
|
{
|
||||||
|
return $this->zoneSignatureIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setZoneSignatureIndex(?int $zoneSignatureIndex): EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
$this->zoneSignatureIndex = $zoneSignatureIndex;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach from the @see{EntityWorkflowStep}.
|
||||||
|
*
|
||||||
|
* @internal used internally to remove the current signature
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function detachEntityWorkflowStep(): self
|
||||||
|
{
|
||||||
|
$this->step = null;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Export;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform data from filter.
|
||||||
|
*
|
||||||
|
* This interface defines a method for transforming filter's form data before it is processed.
|
||||||
|
*
|
||||||
|
* You can implement this interface on @see{FilterInterface} or @see{AggregatorInterface}, to allow to transform existing data in saved exports
|
||||||
|
* and replace it with some default values, or new default values.
|
||||||
|
*/
|
||||||
|
interface DataTransformerInterface
|
||||||
|
{
|
||||||
|
public function transformData(?array $before): array;
|
||||||
|
}
|
@ -32,6 +32,9 @@ interface FilterInterface extends ModifierInterface
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the default data, that can be use as "data" for the form.
|
* Get the default data, that can be use as "data" for the form.
|
||||||
|
*
|
||||||
|
* In case of adding new parameters to a filter, you can implement a @see{DataTransformerFilterInterface} to
|
||||||
|
* transforme the filters's data saved in an export to the desired state.
|
||||||
*/
|
*/
|
||||||
public function getFormDefaultData(): array;
|
public function getFormDefaultData(): array;
|
||||||
|
|
||||||
|
@ -11,7 +11,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type\Export;
|
namespace Chill\MainBundle\Form\Type\Export;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\CallbackTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@ -19,9 +21,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
|||||||
|
|
||||||
class AggregatorType extends AbstractType
|
class AggregatorType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct() {}
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
|
||||||
{
|
{
|
||||||
$exportManager = $options['export_manager'];
|
$exportManager = $options['export_manager'];
|
||||||
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
|
$aggregator = $exportManager->getAggregator($options['aggregator_alias']);
|
||||||
@ -32,17 +32,24 @@ class AggregatorType extends AbstractType
|
|||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$filterFormBuilder = $builder->create('form', FormType::class, [
|
$aggregatorFormBuilder = $builder->create('form', FormType::class, [
|
||||||
'compound' => true,
|
'compound' => true,
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'error_bubbling' => false,
|
'error_bubbling' => false,
|
||||||
]);
|
]);
|
||||||
$aggregator->buildForm($filterFormBuilder);
|
$aggregator->buildForm($aggregatorFormBuilder);
|
||||||
|
|
||||||
$builder->add($filterFormBuilder);
|
if ($aggregator instanceof DataTransformerInterface) {
|
||||||
|
$aggregatorFormBuilder->addViewTransformer(new CallbackTransformer(
|
||||||
|
fn (?array $data) => $data,
|
||||||
|
fn (?array $data) => $aggregator->transformData($data),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
$builder->add($aggregatorFormBuilder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setRequired('aggregator_alias')
|
$resolver->setRequired('aggregator_alias')
|
||||||
->setRequired('export_manager')
|
->setRequired('export_manager')
|
||||||
|
@ -11,8 +11,10 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Form\Type\Export;
|
namespace Chill\MainBundle\Form\Type\Export;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
use Symfony\Component\Form\AbstractType;
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\CallbackTransformer;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
use Symfony\Component\Form\Extension\Core\Type\FormType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@ -41,6 +43,13 @@ class FilterType extends AbstractType
|
|||||||
]);
|
]);
|
||||||
$filter->buildForm($filterFormBuilder);
|
$filter->buildForm($filterFormBuilder);
|
||||||
|
|
||||||
|
if ($filter instanceof DataTransformerInterface) {
|
||||||
|
$filterFormBuilder->addViewTransformer(new CallbackTransformer(
|
||||||
|
fn (?array $data) => $data,
|
||||||
|
fn (?array $data) => $filter->transformData($data),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$builder->add($filterFormBuilder);
|
$builder->add($filterFormBuilder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -12,14 +12,12 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Form;
|
namespace Chill\MainBundle\Form;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStep;
|
|
||||||
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
use Chill\MainBundle\Form\Type\ChillCollectionType;
|
||||||
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
use Chill\MainBundle\Form\Type\ChillTextareaType;
|
||||||
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
use Chill\MainBundle\Form\Type\PickUserDynamicType;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\MainBundle\Workflow\EntityWorkflowManager;
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use Symfony\Component\Form\AbstractType;
|
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\Extension\Core\Type\ChoiceType;
|
||||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
@ -34,18 +32,19 @@ use Symfony\Component\Workflow\Transition;
|
|||||||
|
|
||||||
class WorkflowStepType extends AbstractType
|
class WorkflowStepType extends AbstractType
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EntityWorkflowManager $entityWorkflowManager, private readonly Registry $registry, private readonly TranslatableStringHelperInterface $translatableStringHelper) {}
|
public function __construct(
|
||||||
|
private readonly Registry $registry,
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper
|
||||||
|
) {}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
{
|
{
|
||||||
/** @var EntityWorkflow $entityWorkflow */
|
/** @var EntityWorkflow $entityWorkflow */
|
||||||
$entityWorkflow = $options['entity_workflow'];
|
$entityWorkflow = $options['entity_workflow'];
|
||||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
|
||||||
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
$workflow = $this->registry->get($entityWorkflow, $entityWorkflow->getWorkflowName());
|
||||||
$place = $workflow->getMarking($entityWorkflow);
|
$place = $workflow->getMarking($entityWorkflow);
|
||||||
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
|
$placeMetadata = $workflow->getMetadataStore()->getPlaceMetadata(array_keys($place->getPlaces())[0]);
|
||||||
|
|
||||||
if (true === $options['transition']) {
|
|
||||||
if (null === $options['entity_workflow']) {
|
if (null === $options['entity_workflow']) {
|
||||||
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
throw new \LogicException('if transition is true, entity_workflow should be defined');
|
||||||
}
|
}
|
||||||
@ -131,23 +130,20 @@ class WorkflowStepType extends AbstractType
|
|||||||
];
|
];
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
->add('future_dest_users', PickUserDynamicType::class, [
|
->add('futureDestUsers', PickUserDynamicType::class, [
|
||||||
'label' => 'workflow.dest for next steps',
|
'label' => 'workflow.dest for next steps',
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'mapped' => false,
|
|
||||||
'suggested' => $options['suggested_users'],
|
'suggested' => $options['suggested_users'],
|
||||||
])
|
])
|
||||||
->add('future_cc_users', PickUserDynamicType::class, [
|
->add('futureCcUsers', PickUserDynamicType::class, [
|
||||||
'label' => 'workflow.cc for next steps',
|
'label' => 'workflow.cc for next steps',
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'mapped' => false,
|
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'suggested' => $options['suggested_users'],
|
'suggested' => $options['suggested_users'],
|
||||||
])
|
])
|
||||||
->add('future_dest_emails', ChillCollectionType::class, [
|
->add('futureDestEmails', ChillCollectionType::class, [
|
||||||
'label' => 'workflow.dest by email',
|
'label' => 'workflow.dest by email',
|
||||||
'help' => 'workflow.dest by email help',
|
'help' => 'workflow.dest by email help',
|
||||||
'mapped' => false,
|
|
||||||
'allow_add' => true,
|
'allow_add' => true,
|
||||||
'entry_type' => EmailType::class,
|
'entry_type' => EmailType::class,
|
||||||
'button_add_label' => 'workflow.Add an email',
|
'button_add_label' => 'workflow.Add an email',
|
||||||
@ -160,43 +156,27 @@ class WorkflowStepType extends AbstractType
|
|||||||
'label' => 'Email',
|
'label' => 'Email',
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
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
|
$builder
|
||||||
->add('comment', ChillTextareaType::class, [
|
->add('comment', ChillTextareaType::class, [
|
||||||
'required' => false,
|
'required' => false,
|
||||||
'label' => 'Comment',
|
'label' => 'Comment',
|
||||||
|
'empty_data' => '',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function configureOptions(OptionsResolver $resolver)
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
{
|
{
|
||||||
$resolver
|
$resolver
|
||||||
->setDefined('class')
|
->setDefault('data_class', WorkflowTransitionContextDTO::class)
|
||||||
->setRequired('transition')
|
|
||||||
->setAllowedTypes('transition', 'bool')
|
|
||||||
->setRequired('entity_workflow')
|
->setRequired('entity_workflow')
|
||||||
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
->setAllowedTypes('entity_workflow', EntityWorkflow::class)
|
||||||
->setDefault('suggested_users', [])
|
->setDefault('suggested_users', [])
|
||||||
->setDefault('constraints', [
|
->setDefault('constraints', [
|
||||||
new Callback(
|
new Callback(
|
||||||
function ($step, ExecutionContextInterface $context, $payload) {
|
function (WorkflowTransitionContextDTO $step, ExecutionContextInterface $context, $payload) {
|
||||||
/** @var EntityWorkflowStep $step */
|
$workflow = $this->registry->get($step->entityWorkflow, $step->entityWorkflow->getWorkflowName());
|
||||||
$form = $context->getObject();
|
$transition = $step->transition;
|
||||||
$workflow = $this->registry->get($step->getEntityWorkflow(), $step->getEntityWorkflow()->getWorkflowName());
|
|
||||||
$transition = $form['transition']->getData();
|
|
||||||
$toFinal = true;
|
$toFinal = true;
|
||||||
|
|
||||||
if (null === $transition) {
|
if (null === $transition) {
|
||||||
@ -212,8 +192,8 @@ class WorkflowStepType extends AbstractType
|
|||||||
$toFinal = false;
|
$toFinal = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$destUsers = $form['future_dest_users']->getData();
|
$destUsers = $step->futureDestUsers;
|
||||||
$destEmails = $form['future_dest_emails']->getData();
|
$destEmails = $step->futureDestEmails;
|
||||||
|
|
||||||
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
|
if (!$toFinal && [] === $destUsers && [] === $destEmails) {
|
||||||
$context
|
$context
|
||||||
@ -224,20 +204,6 @@ class WorkflowStepType extends AbstractType
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
new Callback(
|
|
||||||
function ($step, ExecutionContextInterface $context, $payload) {
|
|
||||||
$form = $context->getObject();
|
|
||||||
|
|
||||||
foreach ($form->get('future_dest_users')->getData() as $u) {
|
|
||||||
if (in_array($u, $form->get('future_cc_users')->getData(), true)) {
|
|
||||||
$context
|
|
||||||
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
|
|
||||||
->atPath('ccUsers')
|
|
||||||
->addViolation();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Repository\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Doctrine\Persistence\ObjectRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template-implements ObjectRepository<EntityWorkflowStepSignature>
|
||||||
|
*/
|
||||||
|
class EntityWorkflowStepSignatureRepository implements ObjectRepository
|
||||||
|
{
|
||||||
|
private \Doctrine\ORM\EntityRepository $repository;
|
||||||
|
|
||||||
|
public function __construct(EntityManagerInterface $entityManager)
|
||||||
|
{
|
||||||
|
$this->repository = $entityManager->getRepository(EntityWorkflowStepSignature::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function find($id): ?EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
return $this->repository->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findAll(): array
|
||||||
|
{
|
||||||
|
return $this->repository->findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findBy(array $criteria, ?array $orderBy = null, ?int $limit = null, ?int $offset = null): array
|
||||||
|
{
|
||||||
|
return $this->findBy($criteria, $orderBy, $limit, $offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function findOneBy(array $criteria): ?EntityWorkflowStepSignature
|
||||||
|
{
|
||||||
|
return $this->findOneBy($criteria);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassName(): string
|
||||||
|
{
|
||||||
|
return EntityWorkflowStepSignature::class;
|
||||||
|
}
|
||||||
|
}
|
@ -43,7 +43,14 @@ export const download_report = (url, container) => {
|
|||||||
content = URL.createObjectURL(blob);
|
content = URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
extension = mime.getExtension(type);
|
const extensions = new Map();
|
||||||
|
extensions.set('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xlsx');
|
||||||
|
extensions.set('application/vnd.oasis.opendocument.spreadsheet', 'ods');
|
||||||
|
extensions.set('application/vnd.ms-excel', 'xlsx');
|
||||||
|
extensions.set('text/csv', 'csv');
|
||||||
|
extensions.set('text/csv; charset=utf-8', 'csv');
|
||||||
|
|
||||||
|
extension = extensions.get(type);
|
||||||
|
|
||||||
link.appendChild(document.createTextNode(download_text));
|
link.appendChild(document.createTextNode(download_text));
|
||||||
link.classList.add("btn", "btn-action");
|
link.classList.add("btn", "btn-action");
|
||||||
@ -55,7 +62,7 @@ export const download_report = (url, container) => {
|
|||||||
container.innerHTML = "";
|
container.innerHTML = "";
|
||||||
container.appendChild(link);
|
container.appendChild(link);
|
||||||
}).catch(function(error) {
|
}).catch(function(error) {
|
||||||
console.log(error);
|
console.error(error);
|
||||||
var problem_text =
|
var problem_text =
|
||||||
document.createTextNode("Problem during download");
|
document.createTextNode("Problem during download");
|
||||||
|
|
||||||
|
@ -12,5 +12,5 @@ window.addEventListener("DOMContentLoaded", function(e) {
|
|||||||
container = document.querySelector("#download_container")
|
container = document.querySelector("#download_container")
|
||||||
;
|
;
|
||||||
|
|
||||||
download_report(export_generate_url + "?" + query.toString(), container);
|
download_report(export_generate_url + query.toString(), container);
|
||||||
});
|
});
|
||||||
|
@ -139,7 +139,7 @@ const postprocess = (html: string): string => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const convertMarkdownToHtml = (markdown: string): string => {
|
const convertMarkdownToHtml = (markdown: string): string => {
|
||||||
marked.use({'hooks': {postprocess, preprocess}});
|
marked.use({'hooks': {postprocess, preprocess}, 'async': false});
|
||||||
const rawHtml = marked(markdown) as string;
|
const rawHtml = marked(markdown) as string;
|
||||||
return rawHtml;
|
return rawHtml;
|
||||||
};
|
};
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
{{ 'by_user'|trans ~ ' ' }}
|
{{ 'by_user'|trans ~ ' ' }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="user">
|
<span class="user">
|
||||||
{{ user|chill_entity_render_box(options['user']) }}
|
{{ user|chill_entity_render_box({'at_date': comment.date}) }}
|
||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<span class="chill-entity entity-user">
|
<span class="chill-entity entity-user">
|
||||||
{{- user.label }}
|
{{- user.label }}
|
||||||
{%- if opts['user_job'] and user.userJob(opts['at']) is not null %}
|
{%- if opts['user_job'] and user.userJob(opts['at_date']) is not null %}
|
||||||
<span class="user-job">({{ user.userJob(opts['at']).label|localize_translatable_string }})</span>
|
<span class="user-job">({{ user.userJob(opts['at_date']).label|localize_translatable_string }})</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if opts['main_scope'] and user.mainScope(opts['at']) is not null %}
|
{%- if opts['main_scope'] and user.mainScope(opts['at_date']) is not null %}
|
||||||
<span class="main-scope">({{ user.mainScope(opts['at']).name|localize_translatable_string }})</span>
|
<span class="main-scope">({{ user.mainScope(opts['at_date']).name|localize_translatable_string }})</span>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- if opts['absence'] and user.isAbsent %}
|
{%- if opts['absence'] and user.isAbsent %}
|
||||||
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>
|
<span class="badge bg-danger rounded-pill" title="{{ 'absence.Absent'|trans|escape('html_attr') }}">{{ 'absence.A'|trans }}</span>
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
{% if menus|length > 0 %}
|
||||||
|
<li class="dropdown">
|
||||||
|
<a class="dropdown-toggle btn btn-sm btn-outline-primary"
|
||||||
|
href="#"
|
||||||
|
role="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
<i class="fa fa-flash"></i>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu">
|
||||||
|
{% for menu in menus %}
|
||||||
|
<a class="dropdown-item"
|
||||||
|
href="{{ menu.uri }}"
|
||||||
|
><i class="fa fa-{{- menu.extras.icon }} fa-fw"></i>
|
||||||
|
{{ menu.label|trans }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
@ -21,7 +21,7 @@
|
|||||||
</span>
|
</span>
|
||||||
{% if not c.notification.isSystem %}
|
{% if not c.notification.isSystem %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ c.notification.sender|chill_entity_render_string }}
|
{{ c.notification.sender|chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||||
</span>
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
<span class="badge-user system">{{ 'notification.is_system'|trans }}</span>
|
||||||
@ -53,7 +53,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for a in c.notification.addressees %}
|
{% for a in c.notification.addressees %}
|
||||||
<span class="badge-user">
|
<span class="badge-user">
|
||||||
{{ a|chill_entity_render_string }}
|
{{ a|chill_entity_render_string({'at_date': c.notification.date}) }}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for a in c.notification.addressesEmails %}
|
{% for a in c.notification.addressesEmails %}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "@ChillMain/layout.html.twig" %}
|
{% extends "@ChillMain/layout.html.twig" %}
|
||||||
|
|
||||||
{% block title 'notification.show notification from %sender%'|trans(
|
{% block title 'notification.show notification from %sender%'|trans(
|
||||||
{ '%sender%': notification.sender|chill_entity_render_string }
|
{ '%sender%': notification.sender|chill_entity_render_string({'at_date': notification.date}) }
|
||||||
) ~ ' ' ~ notification.title %}
|
) ~ ' ' ~ notification.title %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
@ -31,14 +31,14 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-12">
|
<div class="col-sm-12">
|
||||||
{{ 'By'|trans }}
|
{{ 'By'|trans }}
|
||||||
{{ step.previous.transitionBy|chill_entity_render_box }},
|
{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }},
|
||||||
{{ step.previous.transitionAt|format_datetime('short', 'short') }}
|
{{ step.previous.transitionAt|format_datetime('short', 'short') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
|
<div class="col-sm-4">{{ 'workflow.Created by'|trans }}</div>
|
||||||
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</div>
|
<div class="col-sm-8">{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt}) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-4">{{ 'Le'|trans }}</div>
|
<div class="col-sm-4">{{ 'Le'|trans }}</div>
|
||||||
@ -58,17 +58,15 @@
|
|||||||
{{ form_row(transition_form.transition) }}
|
{{ form_row(transition_form.transition) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if transition_form.freezeAfter is defined %}
|
|
||||||
{{ form_row(transition_form.freezeAfter) }}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div id="futureDests">
|
<div id="futureDests">
|
||||||
{{ form_row(transition_form.future_dest_users) }}
|
{{ form_row(transition_form.futureDestUsers) }}
|
||||||
|
{{ form_errors(transition_form.futureDestUsers) }}
|
||||||
|
|
||||||
{{ form_row(transition_form.future_cc_users) }}
|
{{ form_row(transition_form.futureCcUsers) }}
|
||||||
|
{{ form_errors(transition_form.futureCcUsers) }}
|
||||||
|
|
||||||
{{ form_row(transition_form.future_dest_emails) }}
|
{{ form_row(transition_form.futureDestEmails) }}
|
||||||
{{ form_errors(transition_form.future_dest_users) }}
|
{{ form_errors(transition_form.futureDestEmails) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>{{ form_label(transition_form.comment) }}</p>
|
<p>{{ form_label(transition_form.comment) }}</p>
|
||||||
@ -110,8 +108,8 @@
|
|||||||
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
|
{% if entity_workflow.currentStep.destUserByAccessKey|length > 0 %}
|
||||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in entity_workflow.currentStep.destUserByAccessKey %}
|
{% for u in entity_workflow.currentStepChained.destUserByAccessKey %}
|
||||||
<li>{{ u|chill_entity_render_box }}</li>
|
<li>{{ u|chill_entity_render_box({'at_date': entity_workflow.currentStepChained.previous.transitionAt }) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -42,7 +42,7 @@
|
|||||||
<div class="item-col" style="width: inherit;">
|
<div class="item-col" style="width: inherit;">
|
||||||
{% if step.transitionBy is not null %}
|
{% if step.transitionBy is not null %}
|
||||||
<div>
|
<div>
|
||||||
{{ step.transitionBy|chill_entity_render_box }}
|
{{ step.transitionBy|chill_entity_render_box({'at_date': step.transitionAt}) }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div>
|
<div>
|
||||||
@ -76,7 +76,7 @@
|
|||||||
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
<p><b>{{ 'workflow.Users allowed to apply transition'|trans }} : </b></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in step.destUser %}
|
{% for u in step.destUser %}
|
||||||
<li>{{ u|chill_entity_render_box }}</li>
|
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -85,7 +85,7 @@
|
|||||||
<p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p>
|
<p><b>{{ 'workflow.Users put in Cc'|trans }} : </b></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in step.ccUser %}
|
{% for u in step.ccUser %}
|
||||||
<li>{{ u|chill_entity_render_box }}</li>
|
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -103,7 +103,7 @@
|
|||||||
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
<p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }} :</b></p>
|
||||||
<ul>
|
<ul>
|
||||||
{% for u in step.destUserByAccessKey %}
|
{% for u in step.destUserByAccessKey %}
|
||||||
<li>{{ u|chill_entity_render_box }}</li>
|
<li>{{ u|chill_entity_render_box({'at_date': step.previous.transitionAt}) }}</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
{% if step.previous is not null %}
|
{% if step.previous is not null %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'By'|trans ~ ' : ' }}</span>
|
||||||
<b>{{ step.previous.transitionBy|chill_entity_render_box }}</b>
|
<b>{{ step.previous.transitionBy|chill_entity_render_box({'at_date': step.previous.transitionAt }) }}</b>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||||
@ -12,19 +12,19 @@
|
|||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.For'|trans ~ ' : ' }}</span>
|
||||||
<b>
|
<b>
|
||||||
{% for d in step.destUser %}{{ d|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
|
{% for d in step.destUser %}{{ d|chill_entity_render_string({'at_date': step.previous.transitionAt}) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
</b>
|
</b>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.Cc'|trans ~ ' : ' }}</span>
|
||||||
<b>
|
<b>
|
||||||
{% for u in step.ccUser %}{{ u|chill_entity_render_string }}{% if not loop.last %}, {% endif %}{% endfor %}
|
{% for u in step.ccUser %}{{ u|chill_entity_render_string({'at_date': step.previous.transitionAt }) }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
</b>
|
</b>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'workflow.Created by'|trans ~ ' : ' }}</span>
|
||||||
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box }}</b>
|
<b>{{ step.entityWorkflow.createdBy|chill_entity_render_box({'at_date': step.entityWorkflow.createdAt }) }}</b>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'Le'|trans ~ ' : ' }}</span>
|
||||||
|
@ -11,8 +11,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Chill\MainBundle\Routing;
|
namespace Chill\MainBundle\Routing;
|
||||||
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
|
||||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
|
||||||
use Twig\Environment;
|
use Twig\Environment;
|
||||||
use Twig\Extension\AbstractExtension;
|
use Twig\Extension\AbstractExtension;
|
||||||
use Twig\TwigFunction;
|
use Twig\TwigFunction;
|
||||||
@ -20,10 +18,8 @@ use Twig\TwigFunction;
|
|||||||
/**
|
/**
|
||||||
* Add the filter 'chill_menu'.
|
* Add the filter 'chill_menu'.
|
||||||
*/
|
*/
|
||||||
class MenuTwig extends AbstractExtension implements ContainerAwareInterface
|
class MenuTwig extends AbstractExtension
|
||||||
{
|
{
|
||||||
private ?ContainerInterface $container = null;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* the default parameters for chillMenu.
|
* the default parameters for chillMenu.
|
||||||
*
|
*
|
||||||
@ -84,9 +80,4 @@ class MenuTwig extends AbstractExtension implements ContainerAwareInterface
|
|||||||
{
|
{
|
||||||
return 'chill_menu';
|
return 'chill_menu';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setContainer(?ContainerInterface $container = null)
|
|
||||||
{
|
|
||||||
$this->container = $container;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -52,7 +52,7 @@ class EntityWorkflowStepNormalizer implements NormalizerAwareInterface, Normaliz
|
|||||||
$data['transitionPreviousBy'] = $this->normalizer->normalize(
|
$data['transitionPreviousBy'] = $this->normalizer->normalize(
|
||||||
$previous->getTransitionBy(),
|
$previous->getTransitionBy(),
|
||||||
$format,
|
$format,
|
||||||
$context
|
[...$context, UserNormalizer::AT_DATE => $previous->getTransitionAt()]
|
||||||
);
|
);
|
||||||
$data['transitionPreviousAt'] = $this->normalizer->normalize(
|
$data['transitionPreviousAt'] = $this->normalizer->normalize(
|
||||||
$previous->getTransitionAt(),
|
$previous->getTransitionAt(),
|
||||||
|
@ -45,7 +45,7 @@ class NotificationNormalizer implements NormalizerAwareInterface, NormalizerInte
|
|||||||
'message' => $object->getMessage(),
|
'message' => $object->getMessage(),
|
||||||
'relatedEntityClass' => $object->getRelatedEntityClass(),
|
'relatedEntityClass' => $object->getRelatedEntityClass(),
|
||||||
'relatedEntityId' => $object->getRelatedEntityId(),
|
'relatedEntityId' => $object->getRelatedEntityId(),
|
||||||
'sender' => $this->normalizer->normalize($object->getSender(), $format, $context),
|
'sender' => $this->normalizer->normalize($object->getSender(), $format, [...$context, UserNormalizer::AT_DATE => $object->getDate()]),
|
||||||
'title' => $object->getTitle(),
|
'title' => $object->getTitle(),
|
||||||
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
|
'entity' => null !== $entity ? $this->normalizer->normalize($entity, $format, $context) : null,
|
||||||
];
|
];
|
||||||
|
@ -19,6 +19,7 @@ use Chill\MainBundle\Entity\User;
|
|||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
use Chill\MainBundle\Templating\Entity\UserRender;
|
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||||
use libphonenumber\PhoneNumber;
|
use libphonenumber\PhoneNumber;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait;
|
||||||
@ -27,6 +28,8 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
{
|
{
|
||||||
use NormalizerAwareTrait;
|
use NormalizerAwareTrait;
|
||||||
|
|
||||||
|
final public const AT_DATE = 'chill:user:at_date';
|
||||||
|
|
||||||
final public const NULL_USER = [
|
final public const NULL_USER = [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => '',
|
'id' => '',
|
||||||
@ -38,10 +41,16 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
'isAbsent' => false,
|
'isAbsent' => false,
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private readonly UserRender $userRender) {}
|
public function __construct(private readonly UserRender $userRender, private readonly ClockInterface $clock) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed|null $format
|
||||||
|
*
|
||||||
|
* @throws \Symfony\Component\Serializer\Exception\ExceptionInterface
|
||||||
|
*/
|
||||||
public function normalize($object, $format = null, array $context = [])
|
public function normalize($object, $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
/** @var array{"chill:user:at_date"?: \DateTimeImmutable|\DateTime} $context */
|
||||||
/** @var User $object */
|
/** @var User $object */
|
||||||
$userJobContext = array_merge(
|
$userJobContext = array_merge(
|
||||||
$context,
|
$context,
|
||||||
@ -72,18 +81,23 @@ class UserNormalizer implements ContextAwareNormalizerInterface, NormalizerAware
|
|||||||
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
return [...self::NULL_USER, 'phonenumber' => $this->normalizer->normalize(null, $format, $phonenumberContext), 'civility' => $this->normalizer->normalize(null, $format, $civilityContext), 'user_job' => $this->normalizer->normalize(null, $format, $userJobContext), 'main_center' => $this->normalizer->normalize(null, $format, $centerContext), 'main_scope' => $this->normalizer->normalize(null, $format, $scopeContext), 'current_location' => $this->normalizer->normalize(null, $format, $locationContext), 'main_location' => $this->normalizer->normalize(null, $format, $locationContext)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$at = $context[self::AT_DATE] ?? $this->clock->now();
|
||||||
|
if ($at instanceof \DateTime) {
|
||||||
|
$at = \DateTimeImmutable::createFromMutable($at);
|
||||||
|
}
|
||||||
|
|
||||||
$data = [
|
$data = [
|
||||||
'type' => 'user',
|
'type' => 'user',
|
||||||
'id' => $object->getId(),
|
'id' => $object->getId(),
|
||||||
'username' => $object->getUsername(),
|
'username' => $object->getUsername(),
|
||||||
'text' => $this->userRender->renderString($object, []),
|
'text' => $this->userRender->renderString($object, ['at_date' => $at]),
|
||||||
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
|
'text_without_absent' => $this->userRender->renderString($object, ['absence' => false]),
|
||||||
'label' => $object->getLabel(),
|
'label' => $object->getLabel(),
|
||||||
'email' => (string) $object->getEmail(),
|
'email' => (string) $object->getEmail(),
|
||||||
'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext),
|
'phonenumber' => $this->normalizer->normalize($object->getPhonenumber(), $format, $phonenumberContext),
|
||||||
'user_job' => $this->normalizer->normalize($object->getUserJob(), $format, $userJobContext),
|
'user_job' => $this->normalizer->normalize($object->getUserJob($at), $format, $userJobContext),
|
||||||
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
'main_center' => $this->normalizer->normalize($object->getMainCenter(), $format, $centerContext),
|
||||||
'main_scope' => $this->normalizer->normalize($object->getMainScope(), $format, $scopeContext),
|
'main_scope' => $this->normalizer->normalize($object->getMainScope($at), $format, $scopeContext),
|
||||||
'isAbsent' => $object->isAbsent(),
|
'isAbsent' => $object->isAbsent(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -12,8 +12,14 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Templating\Entity;
|
namespace Chill\MainBundle\Templating\Entity;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeImmutable;
|
||||||
|
use Symfony\Component\Clock\ClockInterface;
|
||||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Twig\Error\LoaderError;
|
||||||
|
use Twig\Error\RuntimeError;
|
||||||
|
use Twig\Error\SyntaxError;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @implements ChillEntityRenderInterface<User>
|
* @implements ChillEntityRenderInterface<User>
|
||||||
@ -24,15 +30,31 @@ class UserRender implements ChillEntityRenderInterface
|
|||||||
'main_scope' => true,
|
'main_scope' => true,
|
||||||
'user_job' => true,
|
'user_job' => true,
|
||||||
'absence' => true,
|
'absence' => true,
|
||||||
'at' => null,
|
'at_date' => null, // instanceof DateTimeInterface
|
||||||
];
|
];
|
||||||
|
|
||||||
public function __construct(private readonly TranslatableStringHelper $translatableStringHelper, private readonly \Twig\Environment $engine, private readonly TranslatorInterface $translator) {}
|
public function __construct(
|
||||||
|
private readonly TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private readonly \Twig\Environment $engine,
|
||||||
|
private readonly TranslatorInterface $translator,
|
||||||
|
private readonly ClockInterface $clock,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws LoaderError
|
||||||
|
* @throws RuntimeError
|
||||||
|
* @throws SyntaxError
|
||||||
|
*/
|
||||||
public function renderBox($entity, array $options): string
|
public function renderBox($entity, array $options): string
|
||||||
{
|
{
|
||||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||||
|
|
||||||
|
if (null === $opts['at_date']) {
|
||||||
|
$opts['at_date'] = $this->clock->now();
|
||||||
|
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||||
|
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
|
||||||
|
}
|
||||||
|
|
||||||
return $this->engine->render('@ChillMain/Entity/user.html.twig', [
|
return $this->engine->render('@ChillMain/Entity/user.html.twig', [
|
||||||
'user' => $entity,
|
'user' => $entity,
|
||||||
'opts' => $opts,
|
'opts' => $opts,
|
||||||
@ -43,16 +65,24 @@ class UserRender implements ChillEntityRenderInterface
|
|||||||
{
|
{
|
||||||
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
$opts = \array_merge(self::DEFAULT_OPTIONS, $options);
|
||||||
|
|
||||||
$str = $entity->getLabel();
|
// $immutableAtDate = $opts['at_date'] instanceOf DateTime ? DateTimeImmutable::createFromMutable($opts['at_date']) : $opts['at_date'];
|
||||||
|
|
||||||
if (null !== $entity->getUserJob($opts['at']) && $opts['user_job']) {
|
if (null === $opts['at_date']) {
|
||||||
$str .= ' ('.$this->translatableStringHelper
|
$opts['at_date'] = $this->clock->now();
|
||||||
->localize($entity->getUserJob($opts['at'])->getLabel()).')';
|
} elseif ($opts['at_date'] instanceof \DateTime) {
|
||||||
|
$opts['at_date'] = \DateTimeImmutable::createFromMutable($opts['at_date']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (null !== $entity->getMainScope($opts['at']) && $opts['main_scope']) {
|
$str = $entity->getLabel();
|
||||||
|
|
||||||
|
if (null !== $entity->getUserJob($opts['at_date']) && $opts['user_job']) {
|
||||||
$str .= ' ('.$this->translatableStringHelper
|
$str .= ' ('.$this->translatableStringHelper
|
||||||
->localize($entity->getMainScope($opts['at'])->getName()).')';
|
->localize($entity->getUserJob($opts['at_date'])->getLabel()).')';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null !== $entity->getMainScope($opts['at_date']) && $opts['main_scope']) {
|
||||||
|
$str .= ' ('.$this->translatableStringHelper
|
||||||
|
->localize($entity->getMainScope($opts['at_date'])->getName()).')';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($entity->isAbsent() && $opts['absence']) {
|
if ($entity->isAbsent() && $opts['absence']) {
|
||||||
|
@ -338,15 +338,11 @@ abstract class AbstractAggregatorTest extends KernelTestCase
|
|||||||
.'is a string or an be converted to a string', $key)
|
.'is a string or an be converted to a string', $key)
|
||||||
);
|
);
|
||||||
|
|
||||||
$this->assertTrue(
|
$head = \call_user_func($closure, '_header');
|
||||||
// conditions
|
|
||||||
\is_string((string) \call_user_func($closure, '_header'))
|
self::assertIsString($head);
|
||||||
&& !empty(\call_user_func($closure, '_header'))
|
self::assertNotEquals('', $head);
|
||||||
&& '_header' !== \call_user_func($closure, '_header'),
|
self::assertNotEquals('_header', $head);
|
||||||
// message
|
|
||||||
sprintf('Test that the callable return by `getLabels` for key %s '
|
|
||||||
.'can provide an header', $key)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Entity\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflowStepSignature;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
|
||||||
|
use Chill\PersonBundle\Entity\Person;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class EntityWorkflowStepSignatureTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testConstruct()
|
||||||
|
{
|
||||||
|
$workflow = new EntityWorkflow();
|
||||||
|
$workflow->setWorkflowName('vendee_internal')
|
||||||
|
->setRelatedEntityId(0)
|
||||||
|
->setRelatedEntityClass(AccompanyingPeriodWorkEvaluationDocument::class);
|
||||||
|
|
||||||
|
$step = $workflow->getCurrentStep();
|
||||||
|
|
||||||
|
$person = $this->entityManager->createQuery('SELECT p FROM '.Person::class.' p')
|
||||||
|
->setMaxResults(1)
|
||||||
|
->getSingleResult();
|
||||||
|
|
||||||
|
$signature = new EntityWorkflowStepSignature($step, $person);
|
||||||
|
|
||||||
|
self::assertCount(1, $step->getSignatures());
|
||||||
|
self::assertSame($signature, $step->getSignatures()->first());
|
||||||
|
|
||||||
|
$this->entityManager->getConnection()->beginTransaction();
|
||||||
|
$this->entityManager->persist($workflow);
|
||||||
|
$this->entityManager->persist($step);
|
||||||
|
$this->entityManager->persist($signature);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
|
$this->entityManager->getConnection()->commit();
|
||||||
|
|
||||||
|
$this->entityManager->clear();
|
||||||
|
|
||||||
|
$signatureBis = $this->entityManager->find(EntityWorkflowStepSignature::class, $signature->getId());
|
||||||
|
|
||||||
|
self::assertEquals($signature->getId(), $signatureBis->getId());
|
||||||
|
self::assertEquals($step->getId(), $signatureBis->getStep()->getId());
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\MainBundle\Tests\Entity\Workflow;
|
namespace Chill\MainBundle\Tests\Entity\Workflow;
|
||||||
|
|
||||||
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -25,7 +26,7 @@ final class EntityWorkflowTest extends TestCase
|
|||||||
{
|
{
|
||||||
$entityWorkflow = new EntityWorkflow();
|
$entityWorkflow = new EntityWorkflow();
|
||||||
|
|
||||||
$entityWorkflow->setStep('final');
|
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
|
|
||||||
$this->assertTrue($entityWorkflow->isFinal());
|
$this->assertTrue($entityWorkflow->isFinal());
|
||||||
@ -37,16 +38,16 @@ final class EntityWorkflowTest extends TestCase
|
|||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFinal());
|
$this->assertFalse($entityWorkflow->isFinal());
|
||||||
|
|
||||||
$entityWorkflow->setStep('two');
|
$entityWorkflow->setStep('two', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFinal());
|
$this->assertFalse($entityWorkflow->isFinal());
|
||||||
|
|
||||||
$entityWorkflow->setStep('previous_final');
|
$entityWorkflow->setStep('previous_final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFinal());
|
$this->assertFalse($entityWorkflow->isFinal());
|
||||||
|
|
||||||
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
$entityWorkflow->getCurrentStep()->setIsFinal(true);
|
||||||
$entityWorkflow->setStep('final');
|
$entityWorkflow->setStep('final', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertTrue($entityWorkflow->isFinal());
|
$this->assertTrue($entityWorkflow->isFinal());
|
||||||
}
|
}
|
||||||
@ -57,20 +58,20 @@ final class EntityWorkflowTest extends TestCase
|
|||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFreeze());
|
$this->assertFalse($entityWorkflow->isFreeze());
|
||||||
|
|
||||||
$entityWorkflow->setStep('step_one');
|
$entityWorkflow->setStep('step_one', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFreeze());
|
$this->assertFalse($entityWorkflow->isFreeze());
|
||||||
|
|
||||||
$entityWorkflow->setStep('step_three');
|
$entityWorkflow->setStep('step_three', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertFalse($entityWorkflow->isFreeze());
|
$this->assertFalse($entityWorkflow->isFreeze());
|
||||||
|
|
||||||
$entityWorkflow->setStep('freezed');
|
$entityWorkflow->setStep('freezed', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
|
$entityWorkflow->getCurrentStep()->setFreezeAfter(true);
|
||||||
|
|
||||||
$this->assertTrue($entityWorkflow->isFreeze());
|
$this->assertTrue($entityWorkflow->isFreeze());
|
||||||
|
|
||||||
$entityWorkflow->setStep('after_freeze');
|
$entityWorkflow->setStep('after_freeze', new WorkflowTransitionContextDTO($entityWorkflow));
|
||||||
|
|
||||||
$this->assertTrue($entityWorkflow->isFreeze());
|
$this->assertTrue($entityWorkflow->isFreeze());
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ use libphonenumber\PhoneNumberUtil;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Prophecy\Argument;
|
use Prophecy\Argument;
|
||||||
use Prophecy\PhpUnit\ProphecyTrait;
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
use Symfony\Component\Serializer\Exception\ExceptionInterface;
|
||||||
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
|
||||||
|
|
||||||
@ -122,7 +123,9 @@ final class UserNormalizerTest extends TestCase
|
|||||||
$userRender = $this->prophesize(UserRender::class);
|
$userRender = $this->prophesize(UserRender::class);
|
||||||
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');
|
$userRender->renderString(Argument::type(User::class), Argument::type('array'))->willReturn($user ? $user->getLabel() : '');
|
||||||
|
|
||||||
$normalizer = new UserNormalizer($userRender->reveal());
|
$clock = new MockClock(new \DateTimeImmutable('now'));
|
||||||
|
|
||||||
|
$normalizer = new UserNormalizer($userRender->reveal(), $clock);
|
||||||
$normalizer->setNormalizer(new class () implements NormalizerInterface {
|
$normalizer->setNormalizer(new class () implements NormalizerInterface {
|
||||||
public function normalize($object, ?string $format = null, array $context = [])
|
public function normalize($object, ?string $format = null, array $context = [])
|
||||||
{
|
{
|
||||||
|
@ -0,0 +1,109 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Templating\Entity;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Scope;
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
|
use Chill\MainBundle\Templating\Entity\UserRender;
|
||||||
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Prophecy\Argument;
|
||||||
|
use Prophecy\PhpUnit\ProphecyTrait;
|
||||||
|
use Symfony\Component\Clock\MockClock;
|
||||||
|
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||||
|
use Twig\Environment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class UserRenderTest extends TestCase
|
||||||
|
{
|
||||||
|
use ProphecyTrait;
|
||||||
|
|
||||||
|
public function testRenderUserWithJobAndScopeAtCertainDate(): void
|
||||||
|
{
|
||||||
|
// Create a user with a certain user job
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$userJobA = new UserJob();
|
||||||
|
$scopeA = new Scope();
|
||||||
|
|
||||||
|
$userJobA->setLabel(['fr' => 'assistant social'])
|
||||||
|
->setActive(true);
|
||||||
|
$scopeA->setName(['fr' => 'service A']);
|
||||||
|
$user->setLabel('BOB ISLA');
|
||||||
|
|
||||||
|
$userJobB = new UserJob();
|
||||||
|
$scopeB = new Scope();
|
||||||
|
|
||||||
|
$userJobB->setLabel(['fr' => 'directrice'])
|
||||||
|
->setActive(true);
|
||||||
|
$scopeB->setName(['fr' => 'service B']);
|
||||||
|
|
||||||
|
$userJobHistoryA = (new User\UserJobHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setJob($userJobA)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||||
|
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||||
|
|
||||||
|
$userScopeHistoryA = (new User\UserScopeHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setScope($scopeA)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-11-01 12:00:00'))
|
||||||
|
->setEndDate(new \DateTimeImmutable('2023-11-30 00:00:00'));
|
||||||
|
|
||||||
|
$userJobHistoryB = (new User\UserJobHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setJob($userJobB)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
|
||||||
|
|
||||||
|
$userScopeHistoryB = (new User\UserScopeHistory())
|
||||||
|
->setUser($user)
|
||||||
|
->setScope($scopeB)
|
||||||
|
->setStartDate(new \DateTimeImmutable('2023-12-01 12:00:00'));
|
||||||
|
|
||||||
|
$user->getUserJobHistories()->add($userJobHistoryA);
|
||||||
|
$user->getUserScopeHistories()->add($userScopeHistoryA);
|
||||||
|
|
||||||
|
$user->getUserJobHistories()->add($userJobHistoryB);
|
||||||
|
$user->getUserScopeHistories()->add($userScopeHistoryB);
|
||||||
|
|
||||||
|
// Create renderer
|
||||||
|
$translatableStringHelperMock = $this->prophesize(TranslatableStringHelperInterface::class);
|
||||||
|
$translatableStringHelperMock->localize(Argument::type('array'))->will(fn ($args) => $args[0]['fr']);
|
||||||
|
|
||||||
|
$engineMock = $this->createMock(Environment::class);
|
||||||
|
$translatorMock = $this->createMock(TranslatorInterface::class);
|
||||||
|
$clock = new MockClock(new \DateTimeImmutable('2023-12-15 12:00:00'));
|
||||||
|
|
||||||
|
$renderer = new UserRender($translatableStringHelperMock->reveal(), $engineMock, $translatorMock, $clock);
|
||||||
|
|
||||||
|
$optionsNoDate['at_date'] = null;
|
||||||
|
$options['at_date'] = new \DateTime('2023-11-25 12:00:00');
|
||||||
|
$optionsTwo['at_date'] = new \DateTime('2024-01-30 12:00:00');
|
||||||
|
|
||||||
|
// Check that the user render for the first activity corresponds with the first user job
|
||||||
|
$expectedStringA = 'BOB ISLA (assistant social) (service A)';
|
||||||
|
$this->assertEquals($expectedStringA, $renderer->renderString($user, $options));
|
||||||
|
|
||||||
|
// Check that the user render for the second activity corresponds with the second user job
|
||||||
|
$expectedStringB = 'BOB ISLA (directrice) (service B)';
|
||||||
|
$this->assertEquals($expectedStringB, $renderer->renderString($user, $optionsTwo));
|
||||||
|
|
||||||
|
// Check that the user renders the job and scope that is active now, when no date is given
|
||||||
|
$expectedStringC = 'BOB ISLA (directrice) (service B)';
|
||||||
|
$this->assertEquals($expectedStringC, $renderer->renderString($user, $optionsNoDate));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Tests\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Chill\MainBundle\Workflow\EntityWorkflowMarkingStore;
|
||||||
|
use Chill\MainBundle\Workflow\WorkflowTransitionContextDTO;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Component\Workflow\Marking;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class EntityWorkflowMarkingStoreTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testGetMarking(): void
|
||||||
|
{
|
||||||
|
$markingStore = $this->buildMarkingStore();
|
||||||
|
$workflow = new EntityWorkflow();
|
||||||
|
|
||||||
|
$marking = $markingStore->getMarking($workflow);
|
||||||
|
|
||||||
|
self::assertEquals(['initial' => 1], $marking->getPlaces());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSetMarking(): void
|
||||||
|
{
|
||||||
|
$markingStore = $this->buildMarkingStore();
|
||||||
|
$workflow = new EntityWorkflow();
|
||||||
|
|
||||||
|
$dto = new WorkflowTransitionContextDTO($workflow);
|
||||||
|
$dto->futureCcUsers[] = $user1 = new User();
|
||||||
|
$dto->futureDestUsers[] = $user2 = new User();
|
||||||
|
$dto->futureDestEmails[] = $email = 'test@example.com';
|
||||||
|
|
||||||
|
$markingStore->setMarking($workflow, new Marking(['foo' => 1]), ['context' => $dto]);
|
||||||
|
|
||||||
|
$currentStep = $workflow->getCurrentStep();
|
||||||
|
self::assertEquals('foo', $currentStep->getCurrentStep());
|
||||||
|
self::assertContains($email, $currentStep->getDestEmail());
|
||||||
|
self::assertContains($user1, $currentStep->getCcUser());
|
||||||
|
self::assertContains($user2, $currentStep->getDestUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildMarkingStore(): EntityWorkflowMarkingStore
|
||||||
|
{
|
||||||
|
return new EntityWorkflowMarkingStore();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Symfony\Component\Workflow\Marking;
|
||||||
|
use Symfony\Component\Workflow\MarkingStore\MarkingStoreInterface;
|
||||||
|
|
||||||
|
final readonly class EntityWorkflowMarkingStore implements MarkingStoreInterface
|
||||||
|
{
|
||||||
|
public function getMarking(object $subject): Marking
|
||||||
|
{
|
||||||
|
if (!$subject instanceof EntityWorkflow) {
|
||||||
|
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
|
||||||
|
}
|
||||||
|
$step = $subject->getCurrentStep();
|
||||||
|
|
||||||
|
return new Marking([$step->getCurrentStep() => 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setMarking(object $subject, Marking $marking, array $context = []): void
|
||||||
|
{
|
||||||
|
if (!$subject instanceof EntityWorkflow) {
|
||||||
|
throw new \UnexpectedValueException('Expected instance of EntityWorkflow');
|
||||||
|
}
|
||||||
|
|
||||||
|
$places = $marking->getPlaces();
|
||||||
|
if (1 < count($places)) {
|
||||||
|
throw new \LogicException('Expected maximum one place');
|
||||||
|
}
|
||||||
|
$next = array_keys($places)[0];
|
||||||
|
|
||||||
|
$transitionDTO = $context['context'] ?? null;
|
||||||
|
if (!$transitionDTO instanceof WorkflowTransitionContextDTO) {
|
||||||
|
throw new \UnexpectedValueException(sprintf('Expected instance of %s', WorkflowTransitionContextDTO::class));
|
||||||
|
}
|
||||||
|
|
||||||
|
$subject->setStep($next, $transitionDTO);
|
||||||
|
}
|
||||||
|
}
|
@ -21,31 +21,13 @@ use Symfony\Component\Workflow\Event\Event;
|
|||||||
use Symfony\Component\Workflow\Event\GuardEvent;
|
use Symfony\Component\Workflow\Event\GuardEvent;
|
||||||
use Symfony\Component\Workflow\TransitionBlocker;
|
use Symfony\Component\Workflow\TransitionBlocker;
|
||||||
|
|
||||||
class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
final readonly class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly LoggerInterface $chillLogger, private readonly Security $security, private readonly UserRender $userRender) {}
|
public function __construct(
|
||||||
|
private LoggerInterface $chillLogger,
|
||||||
public function addDests(Event $event): void
|
private Security $security,
|
||||||
{
|
private UserRender $userRender
|
||||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
) {}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var EntityWorkflow $entityWorkflow */
|
|
||||||
$entityWorkflow = $event->getSubject();
|
|
||||||
|
|
||||||
foreach ($entityWorkflow->futureCcUsers as $user) {
|
|
||||||
$entityWorkflow->getCurrentStep()->addCcUser($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($entityWorkflow->futureDestUsers as $user) {
|
|
||||||
$entityWorkflow->getCurrentStep()->addDestUser($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($entityWorkflow->futureDestEmails as $email) {
|
|
||||||
$entityWorkflow->getCurrentStep()->addDestEmail($email);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@ -53,7 +35,6 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
|||||||
'workflow.transition' => 'onTransition',
|
'workflow.transition' => 'onTransition',
|
||||||
'workflow.completed' => [
|
'workflow.completed' => [
|
||||||
['markAsFinal', 2048],
|
['markAsFinal', 2048],
|
||||||
['addDests', 2048],
|
|
||||||
],
|
],
|
||||||
'workflow.guard' => [
|
'workflow.guard' => [
|
||||||
['guardEntityWorkflow', 0],
|
['guardEntityWorkflow', 0],
|
||||||
@ -99,6 +80,10 @@ class EntityWorkflowTransitionEventSubscriber implements EventSubscriberInterfac
|
|||||||
|
|
||||||
public function markAsFinal(Event $event): void
|
public function markAsFinal(Event $event): void
|
||||||
{
|
{
|
||||||
|
// NOTE: it is not possible to move this method to the marking store, because
|
||||||
|
// there is dependency between the Workflow definition and the MarkingStoreInterface (the workflow
|
||||||
|
// constructor need a MarkingStoreInterface)
|
||||||
|
|
||||||
if (!$event->getSubject() instanceof EntityWorkflow) {
|
if (!$event->getSubject() instanceof EntityWorkflow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -23,7 +23,13 @@ use Symfony\Component\Workflow\Registry;
|
|||||||
|
|
||||||
class NotificationOnTransition implements EventSubscriberInterface
|
class NotificationOnTransition implements EventSubscriberInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly EntityManagerInterface $entityManager, private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Security $security, private readonly Registry $registry) {}
|
public function __construct(
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
private readonly \Twig\Environment $engine,
|
||||||
|
private readonly MetadataExtractor $metadataExtractor,
|
||||||
|
private readonly Security $security,
|
||||||
|
private readonly Registry $registry
|
||||||
|
) {}
|
||||||
|
|
||||||
public static function getSubscribedEvents(): array
|
public static function getSubscribedEvents(): array
|
||||||
{
|
{
|
||||||
@ -85,7 +91,10 @@ class NotificationOnTransition implements EventSubscriberInterface
|
|||||||
'dest' => $subscriber,
|
'dest' => $subscriber,
|
||||||
'place' => $place,
|
'place' => $place,
|
||||||
'workflow' => $workflow,
|
'workflow' => $workflow,
|
||||||
'is_dest' => \in_array($subscriber->getId(), array_map(static fn (User $u) => $u->getId(), $entityWorkflow->futureDestUsers), true),
|
'is_dest' => \in_array($subscriber->getId(), array_map(
|
||||||
|
static fn (User $u) => $u->getId(),
|
||||||
|
$entityWorkflow->getCurrentStep()->getDestUser()->toArray()
|
||||||
|
), true),
|
||||||
];
|
];
|
||||||
|
|
||||||
$notification = new Notification();
|
$notification = new Notification();
|
||||||
|
@ -20,7 +20,13 @@ use Symfony\Component\Workflow\Registry;
|
|||||||
|
|
||||||
class SendAccessKeyEventSubscriber
|
class SendAccessKeyEventSubscriber
|
||||||
{
|
{
|
||||||
public function __construct(private readonly \Twig\Environment $engine, private readonly MetadataExtractor $metadataExtractor, private readonly Registry $registry, private readonly EntityWorkflowManager $entityWorkflowManager, private readonly MailerInterface $mailer) {}
|
public function __construct(
|
||||||
|
private readonly \Twig\Environment $engine,
|
||||||
|
private readonly MetadataExtractor $metadataExtractor,
|
||||||
|
private readonly Registry $registry,
|
||||||
|
private readonly EntityWorkflowManager $entityWorkflowManager,
|
||||||
|
private readonly MailerInterface $mailer
|
||||||
|
) {}
|
||||||
|
|
||||||
public function postPersist(EntityWorkflowStep $step): void
|
public function postPersist(EntityWorkflowStep $step): void
|
||||||
{
|
{
|
||||||
@ -32,7 +38,7 @@ class SendAccessKeyEventSubscriber
|
|||||||
);
|
);
|
||||||
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
$handler = $this->entityWorkflowManager->getHandler($entityWorkflow);
|
||||||
|
|
||||||
foreach ($entityWorkflow->futureDestEmails as $emailAddress) {
|
foreach ($step->getDestEmail() as $emailAddress) {
|
||||||
$context = [
|
$context = [
|
||||||
'entity_workflow' => $entityWorkflow,
|
'entity_workflow' => $entityWorkflow,
|
||||||
'dest' => $emailAddress,
|
'dest' => $emailAddress,
|
||||||
|
@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\MainBundle\Workflow;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Entity\User;
|
||||||
|
use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
||||||
|
use Symfony\Component\Workflow\Transition;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Context for a transition on an workflow entity.
|
||||||
|
*/
|
||||||
|
class WorkflowTransitionContextDTO
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* a list of future dest users for the next steps.
|
||||||
|
*
|
||||||
|
* This is in used in order to let controller inform who will be the future users which will validate
|
||||||
|
* the next step. This is necessary to perform some computation about the next users, before they are
|
||||||
|
* associated to the entity EntityWorkflowStep.
|
||||||
|
*
|
||||||
|
* @var array|User[]
|
||||||
|
*/
|
||||||
|
public array $futureDestUsers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future cc users for the next steps.
|
||||||
|
*
|
||||||
|
* @var array|User[]
|
||||||
|
*/
|
||||||
|
public array $futureCcUsers = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a list of future dest emails for the next steps.
|
||||||
|
*
|
||||||
|
* This is in used in order to let controller inform who will be the future emails which will validate
|
||||||
|
* the next step. This is necessary to perform some computation about the next emails, before they are
|
||||||
|
* associated to the entity EntityWorkflowStep.
|
||||||
|
*
|
||||||
|
* @var array|string[]
|
||||||
|
*/
|
||||||
|
public array $futureDestEmails = [];
|
||||||
|
|
||||||
|
public ?Transition $transition = null;
|
||||||
|
|
||||||
|
public string $comment = '';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public EntityWorkflow $entityWorkflow
|
||||||
|
) {}
|
||||||
|
|
||||||
|
#[Assert\Callback()]
|
||||||
|
public function validateCCUserIsNotInDest(ExecutionContextInterface $context, $payload): void
|
||||||
|
{
|
||||||
|
foreach ($this->futureDestUsers as $u) {
|
||||||
|
if (in_array($u, $this->futureCcUsers, true)) {
|
||||||
|
$context
|
||||||
|
->buildViolation('workflow.The user in cc cannot be a dest user in the same workflow step')
|
||||||
|
->atPath('ccUsers')
|
||||||
|
->addViolation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,9 +20,5 @@ services:
|
|||||||
|
|
||||||
chill.main.twig.chill_menu:
|
chill.main.twig.chill_menu:
|
||||||
class: Chill\MainBundle\Routing\MenuTwig
|
class: Chill\MainBundle\Routing\MenuTwig
|
||||||
arguments:
|
|
||||||
- "@chill.main.menu_composer"
|
|
||||||
calls:
|
|
||||||
- [setContainer, ["@service_container"]]
|
|
||||||
tags:
|
tags:
|
||||||
- { name: twig.extension }
|
- { name: twig.extension }
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\Migrations\Main;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
final class Version20240628095159 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Add signatures to workflow';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('CREATE SEQUENCE chill_main_workflow_entity_step_signature_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
|
||||||
|
$this->addSql('CREATE TABLE chill_main_workflow_entity_step_signature (id INT NOT NULL, step_id INT NOT NULL, '.
|
||||||
|
'state VARCHAR(50) NOT NULL, stateDate TIMESTAMP(0) WITH TIME ZONE DEFAULT NULL, signatureMetadata JSON DEFAULT \'[]\' NOT NULL,'.
|
||||||
|
' zoneSignatureIndex INT DEFAULT NULL, createdAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updatedAt TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,'.
|
||||||
|
' userSigner_id INT DEFAULT NULL, personSigner_id INT DEFAULT NULL, createdBy_id INT DEFAULT NULL, updatedBy_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C47D4BA3D934E3A4 ON chill_main_workflow_entity_step_signature (userSigner_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C47D4BA3ADFFA293 ON chill_main_workflow_entity_step_signature (personSigner_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C47D4BA373B21E9C ON chill_main_workflow_entity_step_signature (step_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C47D4BA33174800F ON chill_main_workflow_entity_step_signature (createdBy_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_C47D4BA365FF1AEC ON chill_main_workflow_entity_step_signature (updatedBy_id)');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.stateDate IS \'(DC2Type:datetimetz_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.createdAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('COMMENT ON COLUMN chill_main_workflow_entity_step_signature.updatedAt IS \'(DC2Type:datetime_immutable)\'');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3D934E3A4 FOREIGN KEY (userSigner_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA3ADFFA293 FOREIGN KEY (personSigner_id) REFERENCES chill_person_person (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA373B21E9C FOREIGN KEY (step_id) REFERENCES chill_main_workflow_entity_step (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA33174800F FOREIGN KEY (createdBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
$this->addSql('ALTER TABLE chill_main_workflow_entity_step_signature ADD CONSTRAINT FK_C47D4BA365FF1AEC FOREIGN KEY (updatedBy_id) REFERENCES users (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql('DROP SEQUENCE chill_main_workflow_entity_step_signature_id_seq CASCADE');
|
||||||
|
$this->addSql('DROP TABLE chill_main_workflow_entity_step_signature');
|
||||||
|
}
|
||||||
|
}
|
@ -707,19 +707,24 @@ class AccompanyingPeriod implements
|
|||||||
public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection
|
public function getNextCalendarsForPerson(Person $person, $limit = 5): ReadableCollection
|
||||||
{
|
{
|
||||||
$today = new \DateTimeImmutable('today');
|
$today = new \DateTimeImmutable('today');
|
||||||
$criteria = Criteria::create()
|
|
||||||
->where(Criteria::expr()->gte('startDate', $today))
|
|
||||||
// ->andWhere(Criteria::expr()->memberOf('persons', $person))
|
|
||||||
->orderBy(['startDate' => 'DESC'])
|
|
||||||
->setMaxResults($limit * 2);
|
|
||||||
|
|
||||||
return $this->calendars->matching($criteria)
|
$criteria = Criteria::create();
|
||||||
->matching(
|
$expr = Criteria::expr();
|
||||||
// due to a bug, filter two times
|
|
||||||
Criteria::create()
|
$criteria
|
||||||
->where(Criteria::expr()->memberOf('persons', $person))
|
->where(
|
||||||
->setMaxResults($limit)
|
$expr->gte('startDate', $today),
|
||||||
);
|
)
|
||||||
|
->orderBy(['startDate' => 'ASC']);
|
||||||
|
|
||||||
|
$criteriaByPerson = Criteria::create();
|
||||||
|
$criteriaByPerson
|
||||||
|
->where(
|
||||||
|
$expr->memberOf('persons', $person)
|
||||||
|
)
|
||||||
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
return $this->calendars->matching($criteria)->matching($criteriaByPerson);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1332,6 +1337,16 @@ class AccompanyingPeriod implements
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getUserHistories(): ReadableCollection
|
||||||
|
{
|
||||||
|
return $this->userHistories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getCurrentUserHistory(): ?UserHistory
|
||||||
|
{
|
||||||
|
return $this->getUserHistories()->findFirst(fn (int $key, UserHistory $userHistory) => null === $userHistory->getEndDate());
|
||||||
|
}
|
||||||
|
|
||||||
private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self
|
private function addStepHistory(AccompanyingPeriodStepHistory $stepHistory, array $context = []): self
|
||||||
{
|
{
|
||||||
if (!$this->stepHistories->contains($stepHistory)) {
|
if (!$this->stepHistories->contains($stepHistory)) {
|
||||||
|
@ -42,9 +42,10 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
|||||||
/**
|
/**
|
||||||
* @var Collection<AccompanyingPeriodWorkEvaluation>
|
* @var Collection<AccompanyingPeriodWorkEvaluation>
|
||||||
*
|
*
|
||||||
* @internal /!\ the serialization for write evaluations is handled in `AccompanyingPeriodWorkDenormalizer`
|
* @internal the serialization for write evaluations is handled in `accompanyingperiodworkdenormalizer`
|
||||||
|
* @internal the serialization for context docgen:read is handled in `accompanyingperiodworknormalizer`
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['read', 'docgen:read'])]
|
#[Serializer\Groups(['read'])]
|
||||||
#[ORM\OneToMany(targetEntity: AccompanyingPeriodWorkEvaluation::class, mappedBy: 'accompanyingPeriodWork', cascade: ['remove', 'persist'], orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: AccompanyingPeriodWorkEvaluation::class, mappedBy: 'accompanyingPeriodWork', cascade: ['remove', 'persist'], orphanRemoval: true)]
|
||||||
#[ORM\OrderBy(['startDate' => \Doctrine\Common\Collections\Criteria::DESC, 'id' => 'DESC'])]
|
#[ORM\OrderBy(['startDate' => \Doctrine\Common\Collections\Criteria::DESC, 'id' => 'DESC'])]
|
||||||
private Collection $accompanyingPeriodWorkEvaluations;
|
private Collection $accompanyingPeriodWorkEvaluations;
|
||||||
@ -291,18 +292,20 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
|||||||
/**
|
/**
|
||||||
* @return ReadableCollection<int, User>
|
* @return ReadableCollection<int, User>
|
||||||
*/
|
*/
|
||||||
#[Serializer\Groups(['read', 'docgen:read', 'read:accompanyingPeriodWork:light', 'accompanying_period_work:edit', 'accompanying_period_work:create'])]
|
#[Serializer\Groups(['accompanying_period_work:edit'])]
|
||||||
public function getReferrers(): ReadableCollection
|
public function getReferrers(): ReadableCollection
|
||||||
{
|
{
|
||||||
$users = $this->referrersHistory
|
$users = $this->referrersHistory
|
||||||
->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate())
|
->filter(fn (AccompanyingPeriodWorkReferrerHistory $h) => null === $h->getEndDate())
|
||||||
->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser())
|
->map(fn (AccompanyingPeriodWorkReferrerHistory $h) => $h->getUser())
|
||||||
->getValues()
|
->getValues();
|
||||||
;
|
|
||||||
|
|
||||||
return new ArrayCollection(array_values($users));
|
return new ArrayCollection(array_values($users));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Collection<int, AccompanyingPeriodWorkReferrerHistory>
|
||||||
|
*/
|
||||||
public function getReferrersHistory(): Collection
|
public function getReferrersHistory(): Collection
|
||||||
{
|
{
|
||||||
return $this->referrersHistory;
|
return $this->referrersHistory;
|
||||||
@ -470,9 +473,9 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setCreatedBy(?User $createdBy): self
|
public function setCreatedBy(?User $user): self
|
||||||
{
|
{
|
||||||
$this->createdBy = $createdBy;
|
$this->createdBy = $user;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
@ -514,14 +517,14 @@ class AccompanyingPeriodWork implements AccompanyingPeriodLinkedWithSocialIssues
|
|||||||
|
|
||||||
public function setStartDate(\DateTimeInterface $startDate): self
|
public function setStartDate(\DateTimeInterface $startDate): self
|
||||||
{
|
{
|
||||||
$this->startDate = $startDate;
|
$this->startDate = $startDate instanceof \DateTime ? \DateTimeImmutable::createFromMutable($startDate) : $startDate;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface
|
public function setUpdatedAt(\DateTimeInterface $datetime): TrackUpdateInterface
|
||||||
{
|
{
|
||||||
$this->updatedAt = $datetime;
|
$this->updatedAt = $datetime instanceof \DateTime ? \DateTimeImmutable::createFromMutable($datetime) : $datetime;
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
@ -132,6 +132,11 @@ class Comment implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getCreatedBy(): ?User
|
||||||
|
{
|
||||||
|
return $this->getCreator();
|
||||||
|
}
|
||||||
|
|
||||||
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
|
public function setUpdatedAt(\DateTimeInterface $updatedAt): self
|
||||||
{
|
{
|
||||||
$this->updatedAt = $updatedAt;
|
$this->updatedAt = $updatedAt;
|
||||||
|
@ -12,6 +12,7 @@ declare(strict_types=1);
|
|||||||
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
||||||
|
|
||||||
use Chill\MainBundle\Export\AggregatorInterface;
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Repository\UserRepository;
|
use Chill\MainBundle\Repository\UserRepository;
|
||||||
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
@ -21,13 +22,17 @@ use Chill\PersonBundle\Export\Declarations;
|
|||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
final readonly class ReferrerAggregator implements AggregatorInterface
|
final readonly class ReferrerAggregator implements AggregatorInterface, DataTransformerInterface
|
||||||
{
|
{
|
||||||
private const A = 'acp_ref_agg_uhistory';
|
private const A = 'acp_ref_agg_uhistory';
|
||||||
|
|
||||||
private const P = 'acp_ref_agg_date';
|
private const P = 'acp_ref_agg_date';
|
||||||
|
|
||||||
public function __construct(private UserRepository $userRepository, private UserRender $userRender, private RollingDateConverterInterface $rollingDateConverter) {}
|
public function __construct(
|
||||||
|
private UserRepository $userRepository,
|
||||||
|
private UserRender $userRender,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter
|
||||||
|
) {}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
{
|
{
|
||||||
@ -44,18 +49,16 @@ final readonly class ReferrerAggregator implements AggregatorInterface
|
|||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
$qb->expr()->isNull(self::A),
|
$qb->expr()->isNull(self::A),
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->lte(self::A.'.startDate', ':'.self::P),
|
$qb->expr()->lt(self::A.'.startDate', ':'.self::P.'_end_date'),
|
||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
$qb->expr()->isNull(self::A.'.endDate'),
|
$qb->expr()->isNull(self::A.'.endDate'),
|
||||||
$qb->expr()->gt(self::A.'.endDate', ':'.self::P)
|
$qb->expr()->gte(self::A.'.endDate', ':'.self::P.'_start_date')
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
->setParameter(
|
->setParameter(':'.self::P.'_end_date', $this->rollingDateConverter->convert($data['end_date']))
|
||||||
self::P,
|
->setParameter(':'.self::P.'_start_date', $this->rollingDateConverter->convert($data['end_date']));
|
||||||
$this->rollingDateConverter->convert($data['date_calc'])
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyOn(): string
|
public function applyOn(): string
|
||||||
@ -66,15 +69,37 @@ final readonly class ReferrerAggregator implements AggregatorInterface
|
|||||||
public function buildForm(FormBuilderInterface $builder)
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
{
|
{
|
||||||
$builder
|
$builder
|
||||||
->add('date_calc', PickRollingDateType::class, [
|
->add('start_date', PickRollingDateType::class, [
|
||||||
'label' => 'export.aggregator.course.by_referrer.Computation date for referrer',
|
'label' => 'export.aggregator.course.by_referrer.Referrer after',
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.aggregator.course.by_referrer.Until',
|
||||||
'required' => true,
|
'required' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getFormDefaultData(): array
|
public function getFormDefaultData(): array
|
||||||
{
|
{
|
||||||
return ['date_calc' => new RollingDate(RollingDate::T_TODAY)];
|
return [
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformData(?array $before): array
|
||||||
|
{
|
||||||
|
$default = $this->getFormDefaultData();
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
if (null === $before) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['start_date'] = $before['date_calc'] ?? $before['start_date'] ?? $default['start_date'];
|
||||||
|
$data['end_date'] = $before['date_calc'] ?? $before['end_date'] ?? $default['end_date'];
|
||||||
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabels($key, array $values, $data)
|
public function getLabels($key, array $values, $data)
|
||||||
|
@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||||
use Chill\MainBundle\Export\AggregatorInterface;
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
readonly class ReferrerScopeAggregator implements AggregatorInterface
|
readonly class ReferrerScopeAggregator implements AggregatorInterface, DataTransformerInterface
|
||||||
{
|
{
|
||||||
private const PREFIX = 'acp_agg_referrer_scope';
|
private const PREFIX = 'acp_agg_referrer_scope';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private ScopeRepositoryInterface $scopeRepository,
|
private ScopeRepositoryInterface $scopeRepository,
|
||||||
private TranslatableStringHelperInterface $translatableStringHelper,
|
private TranslatableStringHelperInterface $translatableStringHelper,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
@ -46,11 +51,16 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
|
|||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
|
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
|
||||||
$qb->expr()->andX(
|
$qb->expr()->andX(
|
||||||
|
// check that the user is referrer when the accompanying period is opened
|
||||||
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
|
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
|
||||||
$qb->expr()->orX(
|
$qb->expr()->orX(
|
||||||
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
||||||
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
"{$p}_userHistory.startDate <= :{$p}_endDate",
|
||||||
|
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -66,9 +76,15 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
|
|||||||
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
|
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
|
||||||
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
|
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
"{$p}_scopeHistory.startDate <= :{$p}_endDate",
|
||||||
|
"COALESCE({$p}_scopeHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||||
|
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
|
||||||
->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select")
|
->addSelect("IDENTITY({$p}_scopeHistory.scope) AS {$p}_select")
|
||||||
->addGroupBy("{$p}_select");
|
->addGroupBy("{$p}_select");
|
||||||
}
|
}
|
||||||
@ -78,11 +94,36 @@ readonly class ReferrerScopeAggregator implements AggregatorInterface
|
|||||||
return Declarations::ACP_TYPE;
|
return Declarations::ACP_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder) {}
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('start_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.aggregator.course.by_referrer_scope.Referrer and scope after',
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.aggregator.course.by_referrer_scope.Until',
|
||||||
|
'required' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getFormDefaultData(): array
|
public function getFormDefaultData(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformData(?array $before): array
|
||||||
|
{
|
||||||
|
$default = $this->getFormDefaultData();
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
|
||||||
|
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
|
||||||
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabels($key, array $values, $data)
|
public function getLabels($key, array $values, $data)
|
||||||
|
@ -13,20 +13,25 @@ namespace Chill\PersonBundle\Export\Aggregator\AccompanyingCourseAggregators;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||||
use Chill\MainBundle\Export\AggregatorInterface;
|
use Chill\MainBundle\Export\AggregatorInterface;
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Repository\UserJobRepository;
|
use Chill\MainBundle\Repository\UserJobRepository;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
use Doctrine\ORM\Query\Expr\Join;
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
final readonly class UserJobAggregator implements AggregatorInterface
|
final readonly class UserJobAggregator implements AggregatorInterface, DataTransformerInterface
|
||||||
{
|
{
|
||||||
private const PREFIX = 'acp_agg_user_job';
|
private const PREFIX = 'acp_agg_user_job';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private UserJobRepository $jobRepository,
|
private UserJobRepository $jobRepository,
|
||||||
private TranslatableStringHelper $translatableStringHelper
|
private TranslatableStringHelper $translatableStringHelper,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
@ -51,6 +56,10 @@ final readonly class UserJobAggregator implements AggregatorInterface
|
|||||||
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
||||||
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
"{$p}_userHistory.startDate <= :{$p}_endDate",
|
||||||
|
"COALESCE({$p}_userHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -66,9 +75,15 @@ final readonly class UserJobAggregator implements AggregatorInterface
|
|||||||
$qb->expr()->isNull("{$p}_jobHistory.endDate"),
|
$qb->expr()->isNull("{$p}_jobHistory.endDate"),
|
||||||
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
|
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
|
||||||
)
|
)
|
||||||
|
),
|
||||||
|
$qb->expr()->andX(
|
||||||
|
"{$p}_jobHistory.startDate <= :{$p}_endDate",
|
||||||
|
"COALESCE({$p}_jobHistory.endDate, CURRENT_TIMESTAMP()) > :{$p}_startDate"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||||
|
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
|
||||||
->addSelect("IDENTITY({$p}_jobHistory.job) AS {$p}_select")
|
->addSelect("IDENTITY({$p}_jobHistory.job) AS {$p}_select")
|
||||||
->addGroupBy("{$p}_select");
|
->addGroupBy("{$p}_select");
|
||||||
}
|
}
|
||||||
@ -78,11 +93,36 @@ final readonly class UserJobAggregator implements AggregatorInterface
|
|||||||
return Declarations::ACP_TYPE;
|
return Declarations::ACP_TYPE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function buildForm(FormBuilderInterface $builder) {}
|
public function buildForm(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder
|
||||||
|
->add('start_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.aggregator.course.by_referrer_job.Referrer and job after',
|
||||||
|
'required' => true,
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.aggregator.course.by_referrer_job.Until',
|
||||||
|
'required' => true,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
public function getFormDefaultData(): array
|
public function getFormDefaultData(): array
|
||||||
{
|
{
|
||||||
return [];
|
return [
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function transformData(?array $before): array
|
||||||
|
{
|
||||||
|
$default = $this->getFormDefaultData();
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
$data['start_date'] = $before['start_date'] ?? new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01'));
|
||||||
|
$data['end_date'] = $before['end_date'] ?? $default['end_date'];
|
||||||
|
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getLabels($key, array $values, $data)
|
public function getLabels($key, array $values, $data)
|
||||||
|
@ -54,7 +54,6 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
|
|||||||
'socialAction',
|
'socialAction',
|
||||||
'socialIssue',
|
'socialIssue',
|
||||||
'acp_id',
|
'acp_id',
|
||||||
'acp_user',
|
|
||||||
'startDate',
|
'startDate',
|
||||||
'endDate',
|
'endDate',
|
||||||
'goalsId',
|
'goalsId',
|
||||||
@ -70,8 +69,8 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
|
|||||||
'personsName',
|
'personsName',
|
||||||
'thirdParties',
|
'thirdParties',
|
||||||
'handlingThierParty',
|
'handlingThierParty',
|
||||||
// 'acpwReferrers',
|
'acpwReferrers',
|
||||||
'referrers',
|
'referrer',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
@ -156,9 +155,9 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'createdBy', 'updatedBy', 'acp_user' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
'referrers' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'referrer' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
// 'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.' . $key),
|
'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
@ -272,8 +271,7 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
|
|||||||
|
|
||||||
// join acp
|
// join acp
|
||||||
$qb
|
$qb
|
||||||
->addSelect('acp.id AS acp_id')
|
->addSelect('acp.id AS acp_id');
|
||||||
->addSelect('IDENTITY(acp.user) AS acp_user');
|
|
||||||
|
|
||||||
// persons
|
// persons
|
||||||
$qb
|
$qb
|
||||||
@ -282,21 +280,18 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnAccompanyingPeri
|
|||||||
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
|
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
|
||||||
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
|
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
|
||||||
|
|
||||||
// referrers => at date XXXX
|
// referrer => at date XXXX
|
||||||
$qb
|
|
||||||
->addSelect('(SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
|
|
||||||
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrers');
|
|
||||||
|
|
||||||
/*
|
|
||||||
// acpwReferrers at date XXX
|
|
||||||
$qb
|
$qb
|
||||||
->addSelect('(
|
->addSelect('(
|
||||||
SELECT IDENTITY(acpw_ref_history.accompanyingPeriodWork) AS acpw_ref_history_id,
|
SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
|
||||||
JSON_BUILD_OBJECT(\'uid\', IDENTITY(acpw_ref_history.user), \'d\', acpw_ref_history.startDate)
|
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrer');
|
||||||
FROM ' . AccompanyingPeriodWorkReferrerHistory::class . ' acpw_ref_history ' .
|
|
||||||
'WHERE acpw_ref_history.accompanyingPeriodWork = acpw AND acpw_ref_history.startDate <= :calcDate AND (acpw_ref_history.endDate IS NULL or acpw_ref_history.endDate > :calcDate) GROUP BY acpw_ref_history_id) AS acpwReferrers'
|
// acpwReferrer at date XXX
|
||||||
);
|
$qb->addSelect('(SELECT AGGREGATE(IDENTITY(acpwrh.user)) FROM '.AccompanyingPeriodWorkReferrerHistory::class.' acpwrh
|
||||||
*/
|
WHERE acpwrh.accompanyingPeriodWork = acpw
|
||||||
|
AND acpwrh.startDate <= :calcDate AND (acpwrh.endDate IS NULL or acpwrh.endDate > :calcDate)
|
||||||
|
) AS acpwReferrers');
|
||||||
|
$qb->setParameter('calcDate', $calcDate);
|
||||||
|
|
||||||
// thirdparties
|
// thirdparties
|
||||||
$qb
|
$qb
|
||||||
|
@ -54,7 +54,6 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
|
|||||||
'socialAction',
|
'socialAction',
|
||||||
'socialIssue',
|
'socialIssue',
|
||||||
'acp_id',
|
'acp_id',
|
||||||
'acp_user',
|
|
||||||
'startDate',
|
'startDate',
|
||||||
'endDate',
|
'endDate',
|
||||||
'goalsId',
|
'goalsId',
|
||||||
@ -70,8 +69,8 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
|
|||||||
'personsName',
|
'personsName',
|
||||||
'thirdParties',
|
'thirdParties',
|
||||||
'handlingThierParty',
|
'handlingThierParty',
|
||||||
// 'acpwReferrers',
|
'acpwReferrers',
|
||||||
'referrers',
|
'referrer',
|
||||||
'createdAt',
|
'createdAt',
|
||||||
'createdBy',
|
'createdBy',
|
||||||
'updatedAt',
|
'updatedAt',
|
||||||
@ -156,9 +155,9 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
'createdBy', 'updatedBy', 'acp_user' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'createdBy', 'updatedBy' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
'referrers' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'referrer' => $this->userHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
// 'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.' . $key),
|
'acpwReferrers' => $this->userHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
'personsName' => $this->personHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
'handlingThierParty' => $this->thirdPartyHelper->getLabel($key, $values, 'export.list.acpw.'.$key),
|
||||||
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
'thirdParties' => $this->thirdPartyHelper->getLabelMulti($key, $values, 'export.list.acpw.'.$key),
|
||||||
@ -267,8 +266,7 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
|
|||||||
|
|
||||||
// join acp
|
// join acp
|
||||||
$qb
|
$qb
|
||||||
->addSelect('acp.id AS acp_id')
|
->addSelect('acp.id AS acp_id');
|
||||||
->addSelect('IDENTITY(acp.user) AS acp_user');
|
|
||||||
|
|
||||||
// persons
|
// persons
|
||||||
$qb
|
$qb
|
||||||
@ -277,21 +275,17 @@ final readonly class ListAccompanyingPeriodWorkAssociatePersonOnWork implements
|
|||||||
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
|
->addSelect('(SELECT AGGREGATE(person1_acpw_member.id) FROM '.Person::class.' person1_acpw_member '
|
||||||
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
|
.'WHERE person1_acpw_member MEMBER OF acpw.persons) AS personsName');
|
||||||
|
|
||||||
// referrers => at date XXXX
|
// referrer => at date XXXX
|
||||||
$qb
|
$qb
|
||||||
->addSelect('(SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
|
->addSelect('(SELECT JSON_BUILD_OBJECT(\'uid\', IDENTITY(history.user), \'d\', history.startDate) FROM '.UserHistory::class.' history '.
|
||||||
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrers');
|
'WHERE history.accompanyingPeriod = acp AND history.startDate <= :calcDate AND (history.endDate IS NULL OR history.endDate > :calcDate)) AS referrer');
|
||||||
|
|
||||||
/*
|
|
||||||
// acpwReferrers at date XXX
|
// acpwReferrers at date XXX
|
||||||
$qb
|
$qb->addSelect('(SELECT AGGREGATE(IDENTITY(acpwrh.user)) FROM '.AccompanyingPeriodWorkReferrerHistory::class.' acpwrh
|
||||||
->addSelect('(
|
WHERE acpwrh.accompanyingPeriodWork = acpw
|
||||||
SELECT IDENTITY(acpw_ref_history.accompanyingPeriodWork) AS acpw_ref_history_id,
|
AND acpwrh.startDate <= :calcDate AND (acpwrh.endDate IS NULL or acpwrh.endDate > :calcDate)
|
||||||
JSON_BUILD_OBJECT(\'uid\', IDENTITY(acpw_ref_history.user), \'d\', acpw_ref_history.startDate)
|
) AS acpwReferrers');
|
||||||
FROM ' . AccompanyingPeriodWorkReferrerHistory::class . ' acpw_ref_history ' .
|
$qb->setParameter('calcDate', $calcDate);
|
||||||
'WHERE acpw_ref_history.accompanyingPeriodWork = acpw AND acpw_ref_history.startDate <= :calcDate AND (acpw_ref_history.endDate IS NULL or acpw_ref_history.endDate > :calcDate) GROUP BY acpw_ref_history_id) AS acpwReferrers'
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
// thirdparties
|
// thirdparties
|
||||||
$qb
|
$qb
|
||||||
|
@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\User\UserJobHistory;
|
use Chill\MainBundle\Entity\User\UserJobHistory;
|
||||||
use Chill\MainBundle\Entity\UserJob;
|
use Chill\MainBundle\Entity\UserJob;
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
use Chill\MainBundle\Repository\UserJobRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
class UserJobFilter implements FilterInterface
|
final readonly class UserJobFilter implements FilterInterface, DataTransformerInterface
|
||||||
{
|
{
|
||||||
private const PREFIX = 'acp_filter_user_job';
|
private const PREFIX = 'acp_filter_user_job';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
private TranslatableStringHelper $translatableStringHelper,
|
||||||
private readonly UserJobRepositoryInterface $userJobRepository,
|
private UserJobRepositoryInterface $userJobRepository,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
@ -41,42 +46,31 @@ class UserJobFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
$p = self::PREFIX;
|
$p = self::PREFIX;
|
||||||
|
|
||||||
$qb
|
$qb->andWhere(
|
||||||
->leftJoin(
|
$qb->expr()->exists(
|
||||||
'acp.userHistories',
|
sprintf(
|
||||||
"{$p}_userHistory",
|
<<<DQL
|
||||||
Join::WITH,
|
SELECT 1
|
||||||
$qb->expr()->andX(
|
FROM %s {$p}_userHistory
|
||||||
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
|
JOIN %s {$p}_userJobHistory
|
||||||
$qb->expr()->andX(
|
WITH
|
||||||
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
|
{$p}_userHistory.user = {$p}_userJobHistory.user
|
||||||
$qb->expr()->orX(
|
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userJobHistory.startDate, {$p}_userJobHistory.endDate) = TRUE
|
||||||
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
WHERE {$p}_userHistory.accompanyingPeriod = acp
|
||||||
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
AND {$p}_userHistory.startDate <= :{$p}_endDate
|
||||||
)
|
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
|
||||||
)
|
AND {$p}_userJobHistory.startDate <= :{$p}_endDate
|
||||||
)
|
AND ({$p}_userJobHistory.endDate IS NULL OR {$p}_userJobHistory.endDate > :{$p}_startDate)
|
||||||
)
|
AND {$p}_userJobHistory.job IN (:{$p}_jobs)
|
||||||
->leftJoin(
|
DQL,
|
||||||
|
UserHistory::class,
|
||||||
UserJobHistory::class,
|
UserJobHistory::class,
|
||||||
"{$p}_jobHistory",
|
),
|
||||||
Join::WITH,
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->eq("{$p}_jobHistory.user", "{$p}_userHistory.user"),
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->lte("{$p}_jobHistory.startDate", "{$p}_userHistory.startDate"),
|
|
||||||
$qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull("{$p}_jobHistory".'.endDate'),
|
|
||||||
$qb->expr()->gt("{$p}_jobHistory.endDate", "{$p}_userHistory.startDate")
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
->setParameter("{$p}_jobs", $data['jobs'])
|
||||||
)
|
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||||
->andWhere($qb->expr()->in("{$p}_jobHistory.job", ":{$p}_job"))
|
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
|
||||||
->setParameter(
|
|
||||||
"{$p}_job",
|
|
||||||
$data['jobs'],
|
|
||||||
)
|
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,20 +89,29 @@ class UserJobFilter implements FilterInterface
|
|||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
'choice_label' => fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
||||||
'label' => 'Job',
|
'label' => 'Job',
|
||||||
]);
|
])
|
||||||
|
->add('start_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.course.by_user_job.Start from',
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.course.by_user_job.Until',
|
||||||
|
])
|
||||||
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, $format = 'string')
|
public function describeAction($data, $format = 'string')
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'export.filter.course.by_user_job.Filtered by user job: only %job%', [
|
'exports.filter.course.by_user_job.Filtered by user job: only job', [
|
||||||
'%job%' => implode(
|
'job' => implode(
|
||||||
', ',
|
', ',
|
||||||
array_map(
|
array_map(
|
||||||
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
fn (UserJob $job) => $this->translatableStringHelper->localize($job->getLabel()),
|
||||||
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
|
$data['jobs'] instanceof Collection ? $data['jobs']->toArray() : $data['jobs']
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
|
||||||
|
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -117,9 +120,30 @@ class UserJobFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'jobs' => [],
|
'jobs' => [],
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function transformData(?array $before): array
|
||||||
|
{
|
||||||
|
$default = $this->getFormDefaultData();
|
||||||
|
|
||||||
|
if (null === $before) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
|
||||||
|
$before['start_date'] = $default['start_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
|
||||||
|
$before['end_date'] = $default['end_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $before;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
return 'export.filter.course.by_user_job.Filter by user job';
|
return 'export.filter.course.by_user_job.Filter by user job';
|
||||||
|
@ -13,23 +13,28 @@ namespace Chill\PersonBundle\Export\Filter\AccompanyingCourseFilters;
|
|||||||
|
|
||||||
use Chill\MainBundle\Entity\Scope;
|
use Chill\MainBundle\Entity\Scope;
|
||||||
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
use Chill\MainBundle\Entity\User\UserScopeHistory;
|
||||||
|
use Chill\MainBundle\Export\DataTransformerInterface;
|
||||||
use Chill\MainBundle\Export\FilterInterface;
|
use Chill\MainBundle\Export\FilterInterface;
|
||||||
|
use Chill\MainBundle\Form\Type\PickRollingDateType;
|
||||||
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
use Chill\MainBundle\Repository\ScopeRepositoryInterface;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDate;
|
||||||
|
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
|
||||||
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
use Chill\MainBundle\Templating\TranslatableStringHelper;
|
||||||
|
use Chill\PersonBundle\Entity\AccompanyingPeriod\UserHistory;
|
||||||
use Chill\PersonBundle\Export\Declarations;
|
use Chill\PersonBundle\Export\Declarations;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
use Doctrine\ORM\Query\Expr\Join;
|
|
||||||
use Doctrine\ORM\QueryBuilder;
|
use Doctrine\ORM\QueryBuilder;
|
||||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||||
use Symfony\Component\Form\FormBuilderInterface;
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
|
||||||
class UserScopeFilter implements FilterInterface
|
final readonly class UserScopeFilter implements FilterInterface, DataTransformerInterface
|
||||||
{
|
{
|
||||||
private const PREFIX = 'acp_filter_main_scope';
|
private const PREFIX = 'acp_filter_main_scope';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly ScopeRepositoryInterface $scopeRepository,
|
private ScopeRepositoryInterface $scopeRepository,
|
||||||
private readonly TranslatableStringHelper $translatableStringHelper,
|
private TranslatableStringHelper $translatableStringHelper,
|
||||||
|
private RollingDateConverterInterface $rollingDateConverter,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function addRole(): ?string
|
public function addRole(): ?string
|
||||||
@ -37,47 +42,35 @@ class UserScopeFilter implements FilterInterface
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function alterQuery(QueryBuilder $qb, $data)
|
public function alterQuery(QueryBuilder $qb, $data): void
|
||||||
{
|
{
|
||||||
$p = self::PREFIX;
|
$p = self::PREFIX;
|
||||||
|
|
||||||
$qb
|
$qb->andWhere(
|
||||||
->join(
|
$qb->expr()->exists(
|
||||||
'acp.userHistories',
|
sprintf(
|
||||||
"{$p}_userHistory",
|
<<<DQL
|
||||||
Join::WITH,
|
SELECT 1
|
||||||
$qb->expr()->andX(
|
FROM %s {$p}_userHistory
|
||||||
$qb->expr()->eq("{$p}_userHistory.accompanyingPeriod", 'acp.id'),
|
JOIN %s {$p}_userScopeHistory
|
||||||
$qb->expr()->andX(
|
WITH
|
||||||
$qb->expr()->gte('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.startDate"),
|
{$p}_userHistory.user = {$p}_userScopeHistory.user
|
||||||
$qb->expr()->orX(
|
AND OVERLAPSI({$p}_userHistory.startDate, {$p}_userHistory.endDate),({$p}_userScopeHistory.startDate, {$p}_userScopeHistory.endDate) = TRUE
|
||||||
$qb->expr()->isNull("{$p}_userHistory.endDate"),
|
WHERE {$p}_userHistory.accompanyingPeriod = acp
|
||||||
$qb->expr()->lt('COALESCE(acp.closingDate, CURRENT_TIMESTAMP())', "{$p}_userHistory.endDate")
|
AND {$p}_userHistory.startDate <= :{$p}_endDate
|
||||||
)
|
AND ({$p}_userHistory.endDate IS NULL OR {$p}_userHistory.endDate > :{$p}_startDate)
|
||||||
)
|
AND {$p}_userScopeHistory.startDate <= :{$p}_endDate
|
||||||
)
|
AND ({$p}_userScopeHistory.endDate IS NULL OR {$p}_userScopeHistory.endDate > :{$p}_startDate)
|
||||||
)
|
AND {$p}_userScopeHistory.scope IN (:{$p}_scopes)
|
||||||
->join(
|
DQL,
|
||||||
|
UserHistory::class,
|
||||||
UserScopeHistory::class,
|
UserScopeHistory::class,
|
||||||
"{$p}_scopeHistory",
|
),
|
||||||
Join::WITH,
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->eq("{$p}_scopeHistory.user", "{$p}_userHistory.user"),
|
|
||||||
$qb->expr()->andX(
|
|
||||||
$qb->expr()->lte("{$p}_scopeHistory.startDate", "{$p}_userHistory.startDate"),
|
|
||||||
$qb->expr()->orX(
|
|
||||||
$qb->expr()->isNull("{$p}_scopeHistory.endDate"),
|
|
||||||
$qb->expr()->gt("{$p}_scopeHistory.endDate", "{$p}_userHistory.startDate")
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
->setParameter("{$p}_scopes", $data['scopes'])
|
||||||
)
|
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
|
||||||
->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes"))
|
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
|
||||||
->setParameter(
|
|
||||||
"{$p}_scopes",
|
|
||||||
$data['scopes'],
|
|
||||||
)
|
|
||||||
;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function applyOn(): string
|
public function applyOn(): string
|
||||||
@ -94,20 +87,28 @@ class UserScopeFilter implements FilterInterface
|
|||||||
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
'choice_label' => fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
||||||
'multiple' => true,
|
'multiple' => true,
|
||||||
'expanded' => true,
|
'expanded' => true,
|
||||||
|
])
|
||||||
|
->add('start_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.course.by_user_scope.Start from',
|
||||||
|
])
|
||||||
|
->add('end_date', PickRollingDateType::class, [
|
||||||
|
'label' => 'export.filter.course.by_user_scope.Until',
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function describeAction($data, $format = 'string')
|
public function describeAction($data, $format = 'string')
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'export.filter.course.by_user_scope.Filtered by user main scope: only %scope%', [
|
'exports.filter.course.by_user_scope.Filtered by user main scope: only scopes', [
|
||||||
'%scope%' => implode(
|
'scopes' => implode(
|
||||||
', ',
|
', ',
|
||||||
array_map(
|
array_map(
|
||||||
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()),
|
||||||
$data['scopes'] instanceof Collection ? $data['scopes']->toArray() : $data['scopes']
|
$data['scopes'] instanceof Collection ? $data['scopes']->toArray() : $data['scopes']
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
'startDate' => $this->rollingDateConverter->convert($data['start_date']),
|
||||||
|
'endDate' => $this->rollingDateConverter->convert($data['end_date']),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@ -116,9 +117,30 @@ class UserScopeFilter implements FilterInterface
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'scopes' => [],
|
'scopes' => [],
|
||||||
|
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
|
||||||
|
'end_date' => new RollingDate(RollingDate::T_TODAY),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function transformData(?array $before): array
|
||||||
|
{
|
||||||
|
$default = $this->getFormDefaultData();
|
||||||
|
|
||||||
|
if (null === $before) {
|
||||||
|
return $default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('start_date', $before) || null === $before['start_date']) {
|
||||||
|
$before['start_date'] = $default['start_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!array_key_exists('end_date', $before) || null === $before['end_date']) {
|
||||||
|
$before['end_date'] = $default['end_date'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $before;
|
||||||
|
}
|
||||||
|
|
||||||
public function getTitle(): string
|
public function getTitle(): string
|
||||||
{
|
{
|
||||||
return 'export.filter.course.by_user_scope.Filter by user scope';
|
return 'export.filter.course.by_user_scope.Filter by user scope';
|
||||||
|
49
src/Bundle/ChillPersonBundle/Menu/PersonQuickMenuBuilder.php
Normal file
49
src/Bundle/ChillPersonBundle/Menu/PersonQuickMenuBuilder.php
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Chill\PersonBundle\Menu;
|
||||||
|
|
||||||
|
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||||
|
use Chill\PersonBundle\Security\Authorization\AccompanyingPeriodVoter;
|
||||||
|
use Knp\Menu\MenuItem;
|
||||||
|
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||||
|
|
||||||
|
final readonly class PersonQuickMenuBuilder implements LocalMenuBuilderInterface
|
||||||
|
{
|
||||||
|
public function __construct(private AuthorizationCheckerInterface $authorizationChecker) {}
|
||||||
|
|
||||||
|
public static function getMenuIds(): array
|
||||||
|
{
|
||||||
|
return ['person_quick_menu'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||||
|
{
|
||||||
|
/** @var \Chill\PersonBundle\Entity\Person $person */
|
||||||
|
$person = $parameters['person'];
|
||||||
|
|
||||||
|
if ($this->authorizationChecker->isGranted(AccompanyingPeriodVoter::CREATE, $person)) {
|
||||||
|
$menu->addChild(
|
||||||
|
'Create Accompanying Course',
|
||||||
|
[
|
||||||
|
'route' => 'chill_person_accompanying_course_new',
|
||||||
|
'routeParameters' => [
|
||||||
|
'person_id' => [$person->getId()],
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
->setExtras([
|
||||||
|
'order' => 10,
|
||||||
|
'icon' => 'plus',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -30,7 +30,7 @@ div.list-with-period {
|
|||||||
|
|
||||||
// override wrap-list
|
// override wrap-list
|
||||||
div.wrap-list.periods-list {
|
div.wrap-list.periods-list {
|
||||||
padding-right: 1rem;
|
padding-right: 0;
|
||||||
div.wl-row {
|
div.wl-row {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
div.wl-col {
|
div.wl-col {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<a class="btn" :class="getClassButton" :title="$t(buttonTitle)" @click="openModal">
|
<a class="btn" :class="getClassButton" :title="$t(buttonTitle || '')" @click="openModal">
|
||||||
<span v-if="displayTextButton">{{ $t(buttonTitle) }}</span>
|
<span v-if="displayTextButton">{{ $t(buttonTitle || '') }}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<teleport to="body">
|
<teleport to="body">
|
||||||
|
@ -7,10 +7,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<a id="comment-{{ comment.id }}" href="{{ '#comment-' ~ comment.id }}" class="fa fa-pencil-square-o fa-fw"></a>
|
<a id="comment-{{ comment.id }}" href="{{ '#comment-' ~ comment.id }}" class="fa fa-pencil-square-o fa-fw"></a>
|
||||||
|
|
||||||
{% set creator = comment.creator is defined ? comment.creator : comment.createdBy %}
|
{{ 'by'|trans }}
|
||||||
{{ 'by'|trans }}<b>{{ creator }}</b>
|
<span class="badge-user">{{ comment.createdBy|chill_entity_render_box({'at_date': comment.createdAt }) }}</span>{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
|
||||||
|
|
||||||
{{ ', ' ~ 'on'|trans ~ ' ' ~ comment.createdAt|format_date('long') }}<br>
|
|
||||||
<i>{{ 'Last updated on'|trans ~ ' ' ~ comment.updatedAt|format_datetime('long', 'short') }}</i>
|
<i>{{ 'Last updated on'|trans ~ ' ' ~ comment.updatedAt|format_datetime('long', 'short') }}</i>
|
||||||
</div>
|
</div>
|
||||||
<ul class="record_actions">
|
<ul class="record_actions">
|
||||||
|
@ -99,7 +99,7 @@
|
|||||||
<div class="metadata">
|
<div class="metadata">
|
||||||
{{ 'Last updated by'| trans }}
|
{{ 'Last updated by'| trans }}
|
||||||
<span class="user">
|
<span class="user">
|
||||||
{{ accompanyingCourse.pinnedComment.updatedBy|chill_entity_render_box }}
|
{{ accompanyingCourse.pinnedComment.updatedBy|chill_entity_render_box({'at_date': accompanyingCourse.pinnedComment.updatedAt}) }}
|
||||||
</span>
|
</span>
|
||||||
{{ 'on'|trans ~ ' ' }}
|
{{ 'on'|trans ~ ' ' }}
|
||||||
<span class="date">
|
<span class="date">
|
||||||
|
@ -83,9 +83,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="wl-col list">
|
<div class="wl-col list">
|
||||||
{%- if w.referrers|length > 0 -%}
|
{%- if w.referrers|length > 0 -%}
|
||||||
{% for u in w.referrers %}
|
{% for r in w.referrersHistory %}
|
||||||
<span class="wl-item">
|
<span class="wl-item">
|
||||||
<span class="badge-user">{{ u|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ r.user|chill_entity_render_box({'at_date': r.startDate}) }}</span>
|
||||||
{% if not loop.last %}, {% endif %}
|
{% if not loop.last %}, {% endif %}
|
||||||
</span>
|
</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -33,8 +33,8 @@
|
|||||||
{% if w.referrers %}
|
{% if w.referrers %}
|
||||||
<li>
|
<li>
|
||||||
<span class="item-key">{{ 'Referrers'|trans ~ ' : ' }}</span>
|
<span class="item-key">{{ 'Referrers'|trans ~ ' : ' }}</span>
|
||||||
{% for u in w.referrers %}
|
{% for rh in w.referrersHistory %}
|
||||||
<span class="badge-user">{{ u|chill_entity_render_box }}</span>
|
<span class="badge-user">{{ rh.user|chill_entity_render_box({'at_date': rh.startDate}) }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if w.referrers|length == 0 %}
|
{% if w.referrers|length == 0 %}
|
||||||
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
|
<span class="chill-no-data-statement">{{ 'Not given'|trans }}</span>
|
||||||
|
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