Merge remote-tracking branch 'origin/upgrade-sf5' into signature-app-master

This commit is contained in:
Julien Fastré 2024-06-28 10:41:52 +02:00
commit 86c862e69d
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
97 changed files with 1424 additions and 416 deletions

21
.changes/v2.20.0.md Normal file
View 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
View 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
View 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,
* …

View File

@ -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

View File

@ -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"

View File

@ -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
************************ ************************

View File

@ -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",

View 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\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',
])
;
}
}
}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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;
} }

View File

@ -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

View File

@ -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>

View File

@ -18,11 +18,11 @@
<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 -%}
<dd>{{ entity.location.name }}</dd> <dd>{{ entity.location.name }}</dd>

View File

@ -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

View File

@ -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;

View File

@ -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',
])
;
}
}
}

View File

@ -1 +1,2 @@
import './scss/badge.scss'; import './scss/badge.scss';
import './scss/calendar-list.scss';

View File

@ -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;
}
}

View File

@ -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) %}

View File

@ -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;
} }

View File

@ -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é

View File

@ -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;
}

View File

@ -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(),
];
}
} }

View File

@ -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']);

View File

@ -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">

View File

@ -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,
}; };

View File

@ -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,

View File

@ -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']);
}
}

View File

@ -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());

View File

@ -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');

View File

@ -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(),

View File

@ -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[]
*/ */

View File

@ -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;
}

View File

@ -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;

View File

@ -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),
));
}
$builder->add($aggregatorFormBuilder);
} }
public function configureOptions(OptionsResolver $resolver) public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setRequired('aggregator_alias') $resolver->setRequired('aggregator_alias')
->setRequired('export_manager') ->setRequired('export_manager')

View File

@ -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);
} }

View File

@ -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");

View File

@ -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);
}); });

View File

@ -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;
}; };

View File

@ -40,10 +40,10 @@
{{ '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>
{% endif %} {% endif %}
</blockquote> </blockquote>
{{ closing_box|raw }} {{ closing_box|raw }}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>
@ -110,8 +110,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 }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</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 %}

View File

@ -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 }}&nbsp;: </b></p> <p><b>{{ 'workflow.Users allowed to apply transition'|trans }}&nbsp;: </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 }}&nbsp;: </b></p> <p><b>{{ 'workflow.Users put in Cc'|trans }}&nbsp;: </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 }}&nbsp;:</b></p> <p><b>{{ 'workflow.Those users are also granted to apply a transition by using an access key'|trans }}&nbsp;:</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 %}

View File

@ -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>

View File

@ -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;
}
} }

View File

@ -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(),

View File

@ -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,
]; ];

View File

@ -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(),
]; ];

View File

@ -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']) {

View File

@ -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)
);
} }
} }

View File

@ -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 = [])
{ {

View File

@ -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));
}
}

View File

@ -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 }

View File

@ -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)) {

View File

@ -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;
} }

View File

@ -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;

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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,
UserJobHistory::class, UserHistory::class,
"{$p}_jobHistory", UserJobHistory::class,
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")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_jobHistory.job", ":{$p}_job"))
->setParameter(
"{$p}_job",
$data['jobs'],
) )
)
->setParameter("{$p}_jobs", $data['jobs'])
->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']))
; ;
} }
@ -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';

View File

@ -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)
DQL,
UserHistory::class,
UserScopeHistory::class,
),
) )
->join( )
UserScopeHistory::class, ->setParameter("{$p}_scopes", $data['scopes'])
"{$p}_scopeHistory", ->setParameter("{$p}_startDate", $this->rollingDateConverter->convert($data['start_date']))
Join::WITH, ->setParameter("{$p}_endDate", $this->rollingDateConverter->convert($data['end_date']));
$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")
)
)
)
)
->andWhere($qb->expr()->in("{$p}_scopeHistory.scope", ":{$p}_scopes"))
->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';

View 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',
]);
}
}
}

View File

@ -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 {

View File

@ -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">

View File

@ -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">

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -89,7 +89,7 @@
<li> <li>
{% if evaluation.createdBy is not null %} {% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span> <span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b> <b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %} {% endif %}
{% if evaluation.createdAt is not null %} {% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span> <span class="item-key">{{ 'le'|trans }}</span>

View File

@ -13,7 +13,7 @@
{{ 'Last updated by'|trans }} {{ 'Last updated by'|trans }}
{% endif %} {% endif %}
<span class="user"> <span class="user">
{{ entity.updatedBy|chill_entity_render_box }} {{ entity.updatedBy|chill_entity_render_box({'at_date': entity.updatedAt }) }}
</span> </span>
{% endif %} {% endif %}
</div> </div>
@ -34,8 +34,8 @@
{{ 'Created by'|trans }} {{ 'Created by'|trans }}
{% endif %} {% endif %}
<span class="user"> <span class="user">
{{ entity.createdBy|chill_entity_render_string }} {{ entity.createdBy|chill_entity_render_string({'at_date': entity.createdAt}) }}
</span> </span>
{% endif %} {% endif %}
</div> </div>
{% endmacro %} {% endmacro %}

View File

@ -1,13 +1,19 @@
{% macro button_person_before(person) %}
{{ chill_menu('person_quick_menu', {
'layout': '@ChillMain/Menu/quick_menu.html.twig',
'args' : { 'person': person }
}) }}
{% endmacro %}
{% macro button_person_after(person) %} {% macro button_person_after(person) %}
{% set household = person.getCurrentHousehold %} {% set household = person.getCurrentHousehold %}
{% if household is not null and is_granted('CHILL_PERSON_HOUSEHOLD_SEE', household) %} {% if household is not null and is_granted('CHILL_PERSON_HOUSEHOLD_SEE', household) %}
<li> <li>
<a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}" class="btn btn-sm btn-chill-beige"><i class="fa fa-home"></i></a> <a href="{{ path('chill_person_household_summary', { 'household_id': household.id }) }}"
</li> class="btn btn-sm btn-chill-beige"
{% endif %} title="{{ 'Show household'|trans }}"
{% if is_granted('CHILL_PERSON_ACCOMPANYING_PERIOD_CREATE', person) %} ><i class="fa fa-home"></i>
<li> </a>
<a href="{{ path('chill_person_accompanying_course_new', { 'person_id': [ person.id ]}) }}" class="btn btn-sm btn-create change-icon" title="{{ 'Create an accompanying period'|trans }}"><i class="fa fa-random"></i></a>
</li> </li>
{% endif %} {% endif %}
{% endmacro %} {% endmacro %}
@ -165,7 +171,24 @@
<h3>{{ 'chill_calendar.Next calendars'|trans }}</h3> <h3>{{ 'chill_calendar.Next calendars'|trans }}</h3>
</div> </div>
<div class="wl-col list"> <div class="wl-col list">
{% for c in calendars %}<span>{{ c.startDate|format_datetime('long', 'short') }}</span>{% if not loop.last %}, {% endif %}{% endfor %} <div class="calendar-list">
<ul class="calendar-list">
{% for c in calendars %}
<li>
{% if is_granted('CHILL_CALENDAR_CALENDAR_EDIT', c) %}
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_edit', {id: c.id}) }}">
<span class="badge bg-secondary">{{ c.startDate|format_datetime('long', 'short') }}</span>
</a>
{% else %}
<span class="badge bg-secondary">{{ c.startDate|format_datetime('long', 'short') }}</span>
{% endif %}
</li>
{% endfor %}
</ul>
{% if is_granted('CHILL_CALENDAR_CALENDAR_SEE', acp) %}
<a href="{{ chill_path_add_return_path('chill_calendar_calendar_list_by_period', {'id': acp.id}) }}" class="calendar-list__global"><i class="fa fa-list"></i></a>
{% endif %}
</div>
</div> </div>
</div> </div>
@ -184,13 +207,20 @@
</div> </div>
{% endif %} {% endif %}
<ul class="record_actions record_actions_column"> <ul class="record_actions">
{{ chill_menu('accompanying_course_quick_menu', {
'layout': '@ChillMain/Menu/quick_menu.html.twig',
'args' : { 'accompanying-course': acp }
}) }}
<li> <li>
<a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}" <a href="{{ path('chill_person_accompanying_course_index', { 'accompanying_period_id': acp.id }) }}"
class="btn btn-sm btn-outline-primary" title="{{ 'See accompanying period'|trans }}"> class="btn btn-sm btn-primary"
<i class="fa fa-random fa-fw"></i> title="{{ 'See accompanying period'|trans }}"
><i class="fa fa-random fa-fw"></i>
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
@ -247,7 +277,7 @@
'addAltNames': true, 'addAltNames': true,
'addCenter': true, 'addCenter': true,
'address_multiline': false, 'address_multiline': false,
'customButtons': { 'after': _self.button_person_after(person) } 'customButtons': { 'after': _self.button_person_after(person), 'before': _self.button_person_before((person)) }
}) }} }) }}
{#- 'acps' is for AcCompanyingPeriodS #} {#- 'acps' is for AcCompanyingPeriodS #}

View File

@ -296,7 +296,7 @@ This view should receive those arguments:
<div class="created-updated"> <div class="created-updated">
{% if person.createdBy %} {% if person.createdBy %}
<div class="createdBy"> <div class="createdBy">
{{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box }}</b>,<br> {{ 'Created by'|trans}}: <b>{{ person.createdBy|chill_entity_render_box({'at_date': person.createdAt}) }}</b>,<br>
{{ 'on'|trans ~ person.createdAt|format_datetime('long', 'short') }} {{ 'on'|trans ~ person.createdAt|format_datetime('long', 'short') }}
</div> </div>
{% endif %} {% endif %}

View File

@ -64,7 +64,7 @@
<li> <li>
{% if evaluation.createdBy is not null %} {% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span> <span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b> <b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %} {% endif %}
{% if evaluation.createdAt is not null %} {% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span> <span class="item-key">{{ 'le'|trans }}</span>

View File

@ -92,7 +92,7 @@
<li> <li>
{% if evaluation.createdBy is not null %} {% if evaluation.createdBy is not null %}
<span class="item-key">créé par</span> <span class="item-key">créé par</span>
<b>{{ evaluation.createdBy.username }}</b> <b>{{ evaluation.createdBy|chill_entity_render_string({'at_date': evaluation.createdAt}) }}</b>
{% endif %} {% endif %}
{% if evaluation.createdAt is not null %} {% if evaluation.createdAt is not null %}
<span class="item-key">{{ 'le'|trans }}</span> <span class="item-key">{{ 'le'|trans }}</span>

View File

@ -17,6 +17,7 @@ use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher; use Chill\MainBundle\Security\Resolver\ScopeResolverDispatcher;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\PersonBundle\Entity\AccompanyingPeriod; use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation; use Chill\PersonBundle\Entity\AccompanyingPeriodParticipation;
@ -81,6 +82,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
{ {
if ($period instanceof AccompanyingPeriod) { if ($period instanceof AccompanyingPeriod) {
$scopes = $this->scopeResolverDispatcher->isConcerned($period) ? $this->scopeResolverDispatcher->resolveScope($period) : []; $scopes = $this->scopeResolverDispatcher->isConcerned($period) ? $this->scopeResolverDispatcher->resolveScope($period) : [];
$userHistory = $period->getCurrentUserHistory();
if (!\is_array($scopes)) { if (!\is_array($scopes)) {
$scopes = [$scopes]; $scopes = [$scopes];
@ -101,7 +103,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\ClosingMotive::class])), 'closingMotive' => $this->normalizer->normalize($period->getClosingMotive(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\ClosingMotive::class])),
'confidential' => $period->isConfidential(), 'confidential' => $period->isConfidential(),
'createdAt' => $this->normalizer->normalize($period->getCreatedAt(), $format, $dateContext), 'createdAt' => $this->normalizer->normalize($period->getCreatedAt(), $format, $dateContext),
'createdBy' => $this->normalizer->normalize($period->getCreatedBy(), $format, $userContext), 'createdBy' => $this->normalizer->normalize($period->getCreatedBy(), $format, [...$userContext, UserNormalizer::AT_DATE => $period->getCreatedAt()]),
'emergency' => $period->isEmergency(), 'emergency' => $period->isEmergency(),
'openingDate' => $this->normalizer->normalize($period->getOpeningDate(), $format, $dateContext), 'openingDate' => $this->normalizer->normalize($period->getOpeningDate(), $format, $dateContext),
'origin' => $this->normalizer->normalize($period->getOrigin(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Origin::class])), 'origin' => $this->normalizer->normalize($period->getOrigin(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Origin::class])),
@ -123,7 +125,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'isClosed' => null !== $period->getClosingDate(), 'isClosed' => null !== $period->getClosingDate(),
'closingMotiveText' => null !== $period->getClosingMotive() ? 'closingMotiveText' => null !== $period->getClosingMotive() ?
$this->closingMotiveRender->renderString($period->getClosingMotive(), []) : '', $this->closingMotiveRender->renderString($period->getClosingMotive(), []) : '',
'ref' => $this->normalizer->normalize($period->getUser(), $format, $userContext), 'ref' => $this->normalizer->normalize($userHistory?->getUser(), $format, [...$userContext, UserNormalizer::AT_DATE => $userHistory?->getStartDate()]),
'hasRef' => null !== $period->getUser(), 'hasRef' => null !== $period->getUser(),
'socialIssuesText' => implode(', ', array_map(fn (SocialIssue $s) => $this->socialIssueRender->renderString($s, []), $period->getSocialIssues()->toArray())), 'socialIssuesText' => implode(', ', array_map(fn (SocialIssue $s) => $this->socialIssueRender->renderString($s, []), $period->getSocialIssues()->toArray())),
'scopesText' => implode(', ', array_map(fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), $scopes)), 'scopesText' => implode(', ', array_map(fn (Scope $s) => $this->translatableStringHelper->localize($s->getName()), $scopes)),
@ -135,7 +137,7 @@ class AccompanyingPeriodDocGenNormalizer implements ContextAwareNormalizerInterf
'locationPerson' => $this->normalizer->normalize($period->getPersonLocation(), $format, array_merge($context, ['docgen:expects' => Person::class])), 'locationPerson' => $this->normalizer->normalize($period->getPersonLocation(), $format, array_merge($context, ['docgen:expects' => Person::class])),
'location' => $this->normalizer->normalize($period->getLocation(), $format, $addressContext), 'location' => $this->normalizer->normalize($period->getLocation(), $format, $addressContext),
'administrativeLocation' => $this->normalizer->normalize($period->getAdministrativeLocation(), $format, $administrativeLocationContext), 'administrativeLocation' => $this->normalizer->normalize($period->getAdministrativeLocation(), $format, $administrativeLocationContext),
'works' => $this->normalizer->normalize($period->getWorks(), $format, $workContext), 'works' => $this->normalizer->normalize($period->getWorks()->getValues(), $format, $workContext),
'comments' => $this->normalizer->normalize($period->getComments(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])), 'comments' => $this->normalizer->normalize($period->getComments(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])),
'pinnedComment' => $this->normalizer->normalize($period->getPinnedComment(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])), 'pinnedComment' => $this->normalizer->normalize($period->getPinnedComment(), $format, array_merge($context, ['docgen:expects' => AccompanyingPeriod\Comment::class])),
]; ];

View File

@ -11,12 +11,16 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer; namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\User;
use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository; use Chill\MainBundle\Repository\Workflow\EntityWorkflowRepository;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor; use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluation;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkEvaluationDocument;
use Symfony\Component\Serializer\Exception\ExceptionInterface; use Chill\PersonBundle\Entity\SocialWork\SocialAction;
use Chill\ThirdPartyBundle\Entity\ThirdParty;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
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;
@ -30,16 +34,48 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
public function __construct(private readonly Registry $registry, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly MetadataExtractor $metadataExtractor) {} public function __construct(private readonly Registry $registry, private readonly EntityWorkflowRepository $entityWorkflowRepository, private readonly MetadataExtractor $metadataExtractor) {}
/**
* @param AccompanyingPeriodWork $object
*
* @throws ExceptionInterface
*/
public function normalize($object, ?string $format = null, array $context = []): array|\ArrayObject|bool|float|int|string|null public function normalize($object, ?string $format = null, array $context = []): array|\ArrayObject|bool|float|int|string|null
{ {
if (!$object instanceof AccompanyingPeriodWork && 'json' === $format) {
throw new UnexpectedValueException('Object cannot be null or empty when format is json');
}
if ('docgen' === $format && !($object instanceof AccompanyingPeriodWork || null === $object)) {
throw new UnexpectedValueException(sprintf('Object must be an instanceof AccompanyingPeriodWork or null when format is docgen, %s given', get_debug_type($object)));
}
$cleanContext = array_filter($context, fn (string|int $key) => !in_array($key, ['docgen:expects', self::IGNORE_WORK], true), ARRAY_FILTER_USE_KEY);
if (null === $object && 'docgen' === $format) {
$dateNull = $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => \DateTimeImmutable::class]);
$userNull = $this->normalizer->normalize(null, $format, [...$context, 'docgen:expects' => User::class]);
return [
'isNull' => true,
'type' => 'accompanying_period_work',
'accompanyingPeriodWorkEvaluations' => [],
'referrers' => [],
'createdAt' => $dateNull,
'createdAutomatically' => 'false',
'createdAutomaticallyReason' => '',
'createdBy' => $userNull,
'endDate' => $dateNull,
'goals' => [],
'handlingThierParty' => $this->normalizer->normalize(null, $format, [...$cleanContext, 'docgen:expects' => ThirdParty::class]),
'id' => '',
'note' => '',
'persons' => [],
'results' => [],
'socialAction' => $this->normalizer->normalize(null, $format, [...$cleanContext, 'docgen:expects' => SocialAction::class]),
'startDate' => $dateNull,
'thirdParties' => [],
'updatedAt' => $dateNull,
'updatedBy' => $userNull,
];
}
$initial = $this->normalizer->normalize($object, $format, array_merge( $initial = $this->normalizer->normalize($object, $format, array_merge(
$context, $cleanContext,
[self::IGNORE_WORK => spl_object_hash($object)] [self::IGNORE_WORK => null === $object ? null : spl_object_hash($object)]
)); ));
// due to bug: https://api-platform.com/docs/core/serialization/#collection-relation // due to bug: https://api-platform.com/docs/core/serialization/#collection-relation
@ -48,38 +84,57 @@ class AccompanyingPeriodWorkNormalizer implements ContextAwareNormalizerInterfac
$initial['accompanyingPeriodWorkEvaluations'] = $this->normalizer->normalize( $initial['accompanyingPeriodWorkEvaluations'] = $this->normalizer->normalize(
$object->getAccompanyingPeriodWorkEvaluations()->getValues(), $object->getAccompanyingPeriodWorkEvaluations()->getValues(),
$format, $format,
$context [...$cleanContext]
); );
// then, we add normalization for things which are not into the entity // add the referrers
$initial['referrers'] = [];
$initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor( foreach ($object->getReferrersHistory() as $referrerHistory) {
AccompanyingPeriodWork::class, if (null !== $referrerHistory->getEndDate()) {
$object->getId() continue;
); }
$initial['workflows_availables_evaluation'] = $this->metadataExtractor->availableWorkflowFor( $initial['referrers'][] = $this->normalizer->normalize(
AccompanyingPeriodWorkEvaluation::class $referrerHistory->getUser(),
); $format,
[...$cleanContext, UserNormalizer::AT_DATE => $referrerHistory->getStartDate()]
);
}
$initial['workflows_availables_evaluation_documents'] = $this->metadataExtractor->availableWorkflowFor( if ('json' === $format) {
AccompanyingPeriodWorkEvaluationDocument::class // then, we add normalization for things which are not into the entity
); $initial['workflows_availables'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWork::class,
$object->getId()
);
$workflows = $this->entityWorkflowRepository->findBy([ $initial['workflows_availables_evaluation'] = $this->metadataExtractor->availableWorkflowFor(
'relatedEntityClass' => AccompanyingPeriodWork::class, AccompanyingPeriodWorkEvaluation::class
'relatedEntityId' => $object->getId(), );
]);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context); $initial['workflows_availables_evaluation_documents'] = $this->metadataExtractor->availableWorkflowFor(
AccompanyingPeriodWorkEvaluationDocument::class
);
$workflows = $this->entityWorkflowRepository->findBy([
'relatedEntityClass' => AccompanyingPeriodWork::class,
'relatedEntityId' => $object->getId(),
]);
$initial['workflows'] = $this->normalizer->normalize($workflows, 'json', $context);
}
return $initial; return $initial;
} }
public function supportsNormalization($data, ?string $format = null, array $context = []): bool public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{ {
return 'json' === $format return match ($format) {
&& $data instanceof AccompanyingPeriodWork 'json' => $data instanceof AccompanyingPeriodWork && ($context[self::IGNORE_WORK] ?? null) !== spl_object_hash($data),
&& !\array_key_exists(self::IGNORE_WORK, $context); 'docgen' => ($data instanceof AccompanyingPeriodWork || (null === $data && ($context['docgen:expects'] ?? null) === AccompanyingPeriodWork::class))
&& !array_key_exists(self::IGNORE_WORK, $context),
default => false,
};
} }
} }

View File

@ -48,7 +48,7 @@ class SocialActionNormalizer implements NormalizerAwareInterface, NormalizerInte
'type' => 'social_work_social_action', 'type' => 'social_work_social_action',
'text' => $this->render->renderString($socialAction, []), 'text' => $this->render->renderString($socialAction, []),
'title' => $socialAction->getTitle(), 'title' => $socialAction->getTitle(),
'parent' => $this->normalizer->normalize($socialAction->getParent(), $format, $context), 'parent' => $this->normalizer->normalize($socialAction->getParent(), $format, [...$context, 'docgen:expects' => SocialAction::class]),
'issue' => $this->normalizer->normalize($socialAction->getIssue(), $format, $context), 'issue' => $this->normalizer->normalize($socialAction->getIssue(), $format, $context),
]; ];

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer; namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Workflow\EntityWorkflow; use Chill\MainBundle\Entity\Workflow\EntityWorkflow;
use Chill\MainBundle\Serializer\Normalizer\UserNormalizer;
use Chill\MainBundle\Workflow\Helper\MetadataExtractor; use Chill\MainBundle\Workflow\Helper\MetadataExtractor;
use Symfony\Component\Serializer\Exception\ExceptionInterface; use Symfony\Component\Serializer\Exception\ExceptionInterface;
use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface;
@ -46,7 +47,7 @@ class WorkflowNormalizer implements ContextAwareNormalizerInterface, NormalizerA
$data['workflow'] = $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow); $data['workflow'] = $this->metadataExtractor->buildArrayPresentationForWorkflow($workflow);
$data['current_place'] = $this->metadataExtractor->buildArrayPresentationForPlace($object); $data['current_place'] = $this->metadataExtractor->buildArrayPresentationForPlace($object);
$data['current_place_at'] = $this->normalizer->normalize($object->getCurrentStepCreatedAt(), 'json', ['groups' => ['read']]); $data['current_place_at'] = $this->normalizer->normalize($object->getCurrentStepCreatedAt(), 'json', ['groups' => ['read']]);
$data['current_place_by'] = $this->normalizer->normalize($object->getCurrentStepCreatedBy(), 'json', ['groups' => ['read']]); $data['current_place_by'] = $this->normalizer->normalize($object->getCurrentStepCreatedBy(), 'json', ['groups' => ['read'], UserNormalizer::AT_DATE => $object->getCurrentStepCreatedAt()]);
return $data; return $data;
} }

View File

@ -33,7 +33,40 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest
$this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer'); $this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer');
} }
public function getAggregator() /**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
['date_calc' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_TODAY), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
null,
// this is the default configuration
['start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}
public function getAggregator(): ReferrerAggregator
{ {
return $this->aggregator; return $this->aggregator;
} }
@ -41,7 +74,10 @@ final class ReferrerAggregatorTest extends AbstractAggregatorTest
public static function getFormData(): array public static function getFormData(): array
{ {
return [ return [
['date_calc' => new RollingDate(RollingDate::T_TODAY)], [
'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
],
]; ];
} }

View File

@ -14,6 +14,7 @@ namespace Export\Aggregator\AccompanyingCourseAggregators;
use Chill\MainBundle\Entity\Scope; use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Repository\ScopeRepositoryInterface; use Chill\MainBundle\Repository\ScopeRepositoryInterface;
use Chill\MainBundle\Service\RollingDate\RollingDate; use Chill\MainBundle\Service\RollingDate\RollingDate;
use Chill\MainBundle\Service\RollingDate\RollingDateConverter;
use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface; use Chill\MainBundle\Service\RollingDate\RollingDateConverterInterface;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface; use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\MainBundle\Test\Export\AbstractAggregatorTest; use Chill\MainBundle\Test\Export\AbstractAggregatorTest;
@ -48,16 +49,46 @@ final class ReferrerScopeAggregatorTest extends AbstractAggregatorTest
return new ReferrerScopeAggregator( return new ReferrerScopeAggregator(
$scopeRepository->reveal(), $scopeRepository->reveal(),
$translatableStringHelper->reveal(), $translatableStringHelper->reveal(),
$dateConverter->reveal() new RollingDateConverter(),
); );
} }
public static function getFormData(): array public static function getFormData(): array
{ {
return [ return [
[ ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
'date_calc' => new RollingDate(RollingDate::T_TODAY), ];
], }
/**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
null,
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
[],
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
]; ];
} }

View File

@ -33,7 +33,39 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest
$this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer_job'); $this->aggregator = self::getContainer()->get('chill.person.export.aggregator_referrer_job');
} }
public function getAggregator() /**
* @dataProvider provideBeforeData
*/
public function testDataTransformer(?array $before, array $expected): void
{
$actual = $this->getAggregator()->transformData($before);
self::assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach (['start_date', 'end_date'] as $key) {
self::assertInstanceOf(RollingDate::class, $actual[$key]);
self::assertEquals($expected[$key]->getRoll(), $actual[$key]->getRoll(), "Check that the roll is the same for {$key}");
}
}
public static function provideBeforeData(): iterable
{
yield [
null,
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
[],
['start_date' => new RollingDate(RollingDate::T_FIXED_DATE, new \DateTimeImmutable('1970-01-01')), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
yield [
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
];
}
public function getAggregator(): UserJobAggregator
{ {
return $this->aggregator; return $this->aggregator;
} }
@ -41,9 +73,7 @@ final class UserJobAggregatorTest extends AbstractAggregatorTest
public static function getFormData(): array public static function getFormData(): array
{ {
return [ return [
[ ['start_date' => new RollingDate(RollingDate::T_WEEK_CURRENT_START), 'end_date' => new RollingDate(RollingDate::T_TODAY)],
'job_at' => new RollingDate(RollingDate::T_FIXED_DATE, \DateTimeImmutable::createFromFormat('Y-m-d', '2020-01-01')),
],
]; ];
} }

View File

@ -50,11 +50,13 @@ final class UserJobFilterTest extends AbstractFilterTest
$data = []; $data = [];
$data[] = [ $data[] = [
'jobs' => new ArrayCollection($jobs), 'jobs' => new ArrayCollection($jobs),
'date_calc' => new RollingDate(RollingDate::T_TODAY), 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
]; ];
$data[] = [ $data[] = [
'jobs' => $jobs, 'jobs' => $jobs,
'date_calc' => new RollingDate(RollingDate::T_TODAY), 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
]; ];
return $data; return $data;

View File

@ -50,11 +50,13 @@ final class UserScopeFilterTest extends AbstractFilterTest
return [ return [
[ [
'date_calc' => new RollingDate(RollingDate::T_TODAY), 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
'scopes' => new ArrayCollection($scopes), 'scopes' => new ArrayCollection($scopes),
], ],
[ [
'date_calc' => new RollingDate(RollingDate::T_TODAY), 'start_date' => new RollingDate(RollingDate::T_YEAR_CURRENT_START),
'end_date' => new RollingDate(RollingDate::T_TODAY),
'scopes' => $scopes, 'scopes' => $scopes,
], ],
]; ];

View File

@ -11,13 +11,13 @@ declare(strict_types=1);
namespace Serializer\Normalizer; namespace Serializer\Normalizer;
use Chill\DocGeneratorBundle\Test\DocGenNormalizerTestAbstract;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWork;
use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal; use Chill\PersonBundle\Entity\AccompanyingPeriod\AccompanyingPeriodWorkGoal;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\SocialWork\Goal; use Chill\PersonBundle\Entity\SocialWork\Goal;
use Chill\PersonBundle\Entity\SocialWork\Result; use Chill\PersonBundle\Entity\SocialWork\Result;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
@ -25,8 +25,10 @@ use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
* @internal * @internal
* *
* @coversNothing * @coversNothing
*
* @template-extends DocGenNormalizerTestAbstract<AccompanyingPeriodWork>
*/ */
final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase final class AccompanyingPeriodWorkDocGenNormalizerTest extends DocGenNormalizerTestAbstract
{ {
private NormalizerInterface $normalizer; private NormalizerInterface $normalizer;
@ -36,28 +38,31 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
$this->normalizer = self::getContainer()->get(NormalizerInterface::class); $this->normalizer = self::getContainer()->get(NormalizerInterface::class);
} }
public function testNormalizationNull() public function provideNotNullObject(): object
{ {
$actual = $this->normalizer->normalize(null, 'docgen', [ $work = new AccompanyingPeriodWork();
'docgen:expects' => AccompanyingPeriodWork::class, $work
AbstractNormalizer::GROUPS => ['docgen:read'], ->addPerson((new Person())->setFirstName('hello')->setLastName('name'))
]); ->addGoal($g = new AccompanyingPeriodWorkGoal())
->addResult($r = new Result())
->setCreatedAt(new \DateTimeImmutable())
->setUpdatedAt(new \DateTimeImmutable())
->setCreatedBy($user = new User())
->setUpdatedBy($user);
$g->addResult($r)->setGoal($goal = new Goal());
$goal->addResult($r);
$expected = [ return $work;
'id' => '', }
];
$this->assertIsArray($actual); public function provideDocGenExpectClass(): string
$this->markTestSkipped('specification still not finalized'); {
$this->assertEqualsCanonicalizing(array_keys($expected), array_keys($actual)); return AccompanyingPeriodWork::class;
}
foreach ($expected as $key => $item) { public function getNormalizer(): NormalizerInterface
if ('@ignored' === $item) { {
continue; return $this->normalizer;
}
$this->assertEquals($item, $actual[$key]);
}
} }
public function testNormalize() public function testNormalize()
@ -79,20 +84,6 @@ final class AccompanyingPeriodWorkDocGenNormalizerTest extends KernelTestCase
AbstractNormalizer::GROUPS => ['docgen:read'], AbstractNormalizer::GROUPS => ['docgen:read'],
]); ]);
$expected = [
'id' => 0,
];
$this->assertIsArray($actual); $this->assertIsArray($actual);
$this->markTestSkipped('specification still not finalized');
$this->assertEqualsCanonicalizing(array_keys($expected), array_keys($actual));
foreach ($expected as $key => $item) {
if (0 === $item) {
continue;
}
$this->assertEquals($item, $actual[$key]);
}
} }
} }

View File

@ -1,19 +1,9 @@
services: services:
Chill\PersonBundle\Menu\: _defaults:
resource: './../../Menu'
autowire: true
tags:
- { name: 'chill.menu_builder' }
# Chill\PersonBundle\Menu\SectionMenuBuilder:
# arguments:
# $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface'
# $translator: '@Symfony\Contracts\Translation\TranslatorInterface'
# tags:
# - { name: 'chill.menu_builder' }
#
Chill\PersonBundle\Menu\PersonMenuBuilder:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Chill\PersonBundle\Menu\:
resource: './../../Menu'
tags: tags:
- { name: 'chill.menu_builder' } - { name: 'chill.menu_builder' }

View File

@ -143,6 +143,10 @@ exports:
by_referrer_between_dates: by_referrer_between_dates:
description: >- description: >-
Filtré par référent du parcours, entre deux dates: depuis le {start_date, date, medium}, jusqu'au {end_date, date, medium}, seulement {agents} Filtré par référent du parcours, entre deux dates: depuis le {start_date, date, medium}, jusqu'au {end_date, date, medium}, seulement {agents}
by_user_job:
"Filtered by user job: only job": "Filtré par métier du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {job}"
by_user_scope:
"Filtered by user main scope: only scopes": "Filtré par service du référent entre le {startDate, date, short} et le {endDate, date, short}: uniquement {scopes}"
work: work:
by_treating_agent: by_treating_agent:
Filtered by treating agent at date: >- Filtered by treating agent at date: >-

View File

@ -1058,9 +1058,15 @@ export:
by-user: by-user:
title: Grouper les parcours par usager participant title: Grouper les parcours par usager participant
header: Usager participant header: Usager participant
by_referrer: by_referrer:
Computation date for referrer: Date à laquelle le référent était actif Referrer after: Référent après le
Until: Jusqu'au
by_referrer_scope:
Referrer and scope after: Référent et service après le
Until: Jusqu'au
by_referrer_job:
Referrer and job after: Référent et métier après le
Until: Jusqu'au
by_user_scope: by_user_scope:
Group course by referrer's scope: Grouper les parcours par service du référent Group course by referrer's scope: Grouper les parcours par service du référent
Referrer's scope: Service du référent de parcours Referrer's scope: Service du référent de parcours
@ -1215,7 +1221,8 @@ export:
'Filtered by steps: only %step% and between %date_from% and %date_to%': 'Filtré par statut: seulement %step%, entre %date_from% et %date_to%' 'Filtered by steps: only %step% and between %date_from% and %date_to%': 'Filtré par statut: seulement %step%, entre %date_from% et %date_to%'
by_user_scope: by_user_scope:
Filter by user scope: Filtrer les parcours par service du référent Filter by user scope: Filtrer les parcours par service du référent
"Filtered by user main scope: only %scope%": "Filtré par service du référent: uniquement %scope%" Start from: Référent et service depuis le
Until: Jusqu'au
by_referrer: by_referrer:
Computation date for referrer: Date à laquelle le référent était actif Computation date for referrer: Date à laquelle le référent était actif
by_referrer_between_dates: by_referrer_between_dates:
@ -1232,7 +1239,8 @@ export:
'Filtered by creator job: only %jobs%': "Filtré par métier du créateur: uniquement %jobs%" 'Filtered by creator job: only %jobs%': "Filtré par métier du créateur: uniquement %jobs%"
by_user_job: by_user_job:
Filter by user job: Filtrer les parcours par métier du référent Filter by user job: Filtrer les parcours par métier du référent
"Filtered by user job: only %job%": "Filtré par métier du référent: uniquement %job%" Start from: Référent et métier depuis le
Until: Jusqu'au
by_social_action: by_social_action:
title: Filtrer les parcours par action d'accompagnement title: Filtrer les parcours par action d'accompagnement
Accepted socialactions: Actions d'accompagnement Accepted socialactions: Actions d'accompagnement
@ -1405,7 +1413,8 @@ export:
updatedBy: Modifié par updatedBy: Modifié par
acp_id: Identifiant du parcours acp_id: Identifiant du parcours
acp_user: Référent du parcours acp_user: Référent du parcours
referrers: Agents traitants acpwReferrers: Agents traitants
referrer: Référent du parcours
personsId: Identifiants des usagers personsId: Identifiants des usagers
personsName: Usagers de l'action personsName: Usagers de l'action
goalsId: Identifiants des objectifs goalsId: Identifiants des objectifs

View File

@ -85,7 +85,6 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
/** /**
* [fr] Sigle. * [fr] Sigle.
*/ */
#[Assert\Length(min: 2)]
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])] #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'acronym', type: \Doctrine\DBAL\Types\Types::STRING, length: 64, nullable: true)] #[ORM\Column(name: 'acronym', type: \Doctrine\DBAL\Types\Types::STRING, length: 64, nullable: true)]
private ?string $acronym = ''; private ?string $acronym = '';
@ -184,7 +183,6 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface, \Strin
/** /**
* [fr] Raison sociale. * [fr] Raison sociale.
*/ */
#[Assert\Length(min: 3)]
#[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])] #[Groups(['read', 'write', 'docgen:read', 'docgen:read:3party:parent'])]
#[ORM\Column(name: 'name_company', type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)] #[ORM\Column(name: 'name_company', type: \Doctrine\DBAL\Types\Types::STRING, length: 255, nullable: true)]
private ?string $nameCompany = ''; private ?string $nameCompany = '';

View File

@ -23,7 +23,7 @@ if (is_array($env = @include dirname(__DIR__).'/.env.local.php') && (!isset($env
(new Dotenv(false))->populate($env); (new Dotenv(false))->populate($env);
} else { } else {
// load all the .env files // load all the .env files
(new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env.test'); (new Dotenv(false))->loadEnv(dirname(__DIR__).'/../../.env');
} }
$_SERVER += $_ENV; $_SERVER += $_ENV;