diff --git a/CHANGELOG.md b/CHANGELOG.md
index cb5805ca3..952757724 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,10 +12,37 @@ and this project adheres to
* [person] household address: add a form for editing the validFrom date (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/541)
+* [docgen] add more persons choices in docgen for course: amongst requestor (if person), resources of course (if person), and PersonResource (if person);
+* [docgen] add a new context with a list of activities in course
+* [docgen] add a comment in budget lines
+* [notifications] allow to send a notification to an email address. The address receive an access link
+
+## Test releases
+
+### 2021-04-07
+
+* notification list: move action buttons outside of the toggle
+* fix detecting of non-read notification
+* filter users which are disabled in search user api
+* order query for location and add pagination in list
+* allow every person which has part for a workflow to see the workflow page
+* able to see the workflow if the evaluation document has been deleted
+* hardcode the list of supported mime types for edition with collabora
+* list of accompanying course: allow to see the pinned comment in list_item
+
+### 2021-04-06
+
+* [main] notification toggle read: correct js syntax for compilation in production (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/548)
+* [parcours] Display of interlocuteurs changed to flex-table in parcours edit page to prevent cut-off of information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/535)
+* [activity] espace entre les boutons pour supprimer les documents
+
+
+### continuous release in February and March
+
+* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
* [person] Accompanying course evaluation documents: disable the WOPI edit link if mimetype not supported and if no keyInfos
(https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/585)
-
* [activity] display error messages above the form in creating a new location (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/481)
* [activity] show required field in activity edit/new by an asterix in the vuejs fields (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/494)
* [ACL] fix allow to see the course, event if the scope'course does not contains the scope's user
@@ -79,8 +106,7 @@ and this project adheres to
* [parcours] Create document buttons made sticky (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/532)
* [person] Trailing guillemet removed (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/530)
* [notification] Display of social action within workflow notification set to display block (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/537)
-
-## Test releases
+* [onthefly] trim trailing whitespace in email of person and thirdparty (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/542)
### test release 2022-02-21
@@ -108,8 +134,6 @@ and this project adheres to
* [bug]: fix confidential toggle of address in thirdpartyrenderbox (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/460)
-## Test releases
-* Creation of PickCivilityType, and implementation in PersonType and ThirdpartyType
### test release 2022-02-14
@@ -136,6 +160,7 @@ and this project adheres to
* [address]: Correction residential address 'depuis le' (https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/459)
* [Documents]: List view adapted to display more information (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/414)
* [Thirdparty_contact]: address blurred if confidential in view page (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/450)
+* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
### test release 2021-02-01
@@ -158,18 +183,9 @@ and this project adheres to
* [fast_actions] improve fast-actions buttons override mechanism, fix https://gitlab.com/champs-libres/departement-de-la-vendee/accent-suivi-developpement/-/issues/413
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
-
-
-## Test releases
-=======
* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
->>>>>>> issue422_and_others_on_AddPersons
-=======
-* [homepage widget] add vue homepage_widget with asynchone loading, give a global view resume of the user concerned actions, notifications, etc.
-* [thirdparty] Add a contact to a thirdparty from within onTheFly (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/345)
->>>>>>> b0d50d315c8e00959a967badac9cf5057ab2b4bc
### test release 2021-01-31
* [person] accompanying course: optimisation: do not fetch some resources for the banner (https://gitlab.com/champs-libres/departement-de-la-vendee/chill/-/issues/409)
diff --git a/phpstan-types.neon b/phpstan-types.neon
index 9671c05a5..dc3f67a73 100644
--- a/phpstan-types.neon
+++ b/phpstan-types.neon
@@ -450,18 +450,8 @@ parameters:
count: 1
path: src/Bundle/ChillThirdPartyBundle/Repository/ThirdPartyRepository.php
- -
- message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
- count: 1
- path: src/Bundle/ChillThirdPartyBundle/Search/ThirdPartyApiSearch.php
-
-
message: "#^Method Chill\\\\ThirdPartyBundle\\\\Search\\\\ThirdPartySearch\\:\\:renderResult\\(\\) should return string but return statement is missing\\.$#"
count: 1
path: src/Bundle/ChillThirdPartyBundle/Search/ThirdPartySearch.php
- -
- message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
- count: 1
- path: src/Bundle/ChillThirdPartyBundle/Templating/Entity/ThirdPartyRender.php
-
diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php
index 48f750c26..d960a0c21 100644
--- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php
+++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepository.php
@@ -12,13 +12,20 @@ declare(strict_types=1);
namespace Chill\ActivityBundle\Repository;
use Chill\ActivityBundle\Entity\Activity;
+use Chill\ActivityBundle\Entity\ActivityPresence;
+use Chill\ActivityBundle\Entity\ActivityType;
use Chill\ActivityBundle\Security\Authorization\ActivityVoter;
+use Chill\MainBundle\Entity\Location;
+use Chill\MainBundle\Entity\LocationType;
use Chill\MainBundle\Entity\Scope;
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
use Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface;
use Chill\PersonBundle\Entity\AccompanyingPeriod;
use Chill\PersonBundle\Entity\Person;
+use Doctrine\DBAL\Types\Types;
+use Doctrine\ORM\AbstractQuery;
use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\Query\ResultSetMappingBuilder;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Role\Role;
use Symfony\Component\Security\Core\Security;
@@ -72,6 +79,86 @@ final class ActivityACLAwareRepository implements ActivityACLAwareRepositoryInte
->findByAccompanyingPeriod($period, $scopes, true, $limit, $start, $orderBy);
}
+ public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array
+ {
+ $rsm = new ResultSetMappingBuilder($this->em);
+
+ $sql = '
+ SELECT
+ a.id AS activity_id,
+ date,
+ CASE WHEN durationtime IS NOT NULL THEN (EXTRACT(EPOCH from durationtime) / 60)::int ELSE 0 END AS durationtimeminute,
+ attendee_id,
+ comment_comment,
+ emergency,
+ sentreceived,
+ CASE WHEN traveltime IS NOT NULL THEN (EXTRACT(EPOCH from traveltime) / 60)::int ELSE 0 END AS traveltimeminute,
+ t.id AS type_id, t.name as type_name,
+ p.id AS presence_id, p.name AS presence_name,
+ location.id AS location_id, location.address_id, location.name AS location_name, location.phonenumber1, location.phonenumber2, location.email,
+ location.locationtype_id, locationtype.title AS locationtype_title,
+ users.userids AS userids,
+ thirdparties.thirdpartyids,
+ persons.personids,
+ actions.socialactionids,
+ issues.socialissueids
+
+ FROM activity a
+ LEFT JOIN chill_main_location location ON a.location_id = location.id
+ LEFT JOIN chill_main_location_type locationtype ON location.locationtype_id = locationtype.id
+ LEFT JOIN activitytpresence p ON a.attendee_id = p.id
+ LEFT JOIN activitytype t ON a.type_id = t.id
+ LEFT JOIN LATERAL (SELECT jsonb_agg(user_id) userids, activity_id FROM activity_user AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS users ON TRUE
+ LEFT JOIN LATERAL (SELECT jsonb_agg(thirdparty_id) thirdpartyids, activity_id FROM activity_thirdparty AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS thirdparties ON TRUE
+ LEFT JOIN LATERAL (SELECT jsonb_agg(person_id) personids, activity_id FROM activity_person AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS persons ON TRUE
+ LEFT JOIN LATERAL (SELECT jsonb_agg(socialaction_id) socialactionids, activity_id FROM chill_activity_activity_chill_person_socialaction AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS actions ON TRUE
+ LEFT JOIN LATERAL (SELECT jsonb_agg(socialissue_id) socialissueids, activity_id FROM chill_activity_activity_chill_person_socialissue AS au WHERE a.id = au.activity_id GROUP BY activity_id) AS issues ON TRUE
+
+ WHERE accompanyingperiod_id = ?
+ ORDER BY a.date DESC, a.id DESC
+ LIMIT ?
+ ';
+
+ $rsm
+ ->addEntityResult(Activity::class, 'a')
+ ->addFieldResult('a', 'activity_id', 'id')
+ ->addFieldResult('a', 'date', 'date')
+ ->addFieldResult('a', 'comment', 'comment')
+ ->addFieldResult('a', 'sentreceived', 'sentReceived')
+ ->addFieldResult('a', 'emergency', 'emergency')
+ ->addJoinedEntityResult(Location::class, 'location', 'a', 'location')
+ ->addFieldResult('location', 'location_id', 'id')
+ ->addFieldResult('location', 'location_name', 'name')
+ ->addFieldResult('location', 'phonenumber1', 'phonenumber1')
+ ->addFieldResult('location', 'phonenumber2', 'phonenumber2')
+ ->addFieldResult('location', 'email', 'email')
+ ->addJoinedEntityResult(LocationType::class, 'locationType', 'location', 'locationType')
+ ->addFieldResult('locationType', 'locationtype_id', 'id')
+ ->addFieldResult('locationType', 'locationtype_title', 'title')
+ ->addJoinedEntityResult(ActivityType::class, 'activityType', 'a', 'activityType')
+ ->addFieldResult('activityType', 'type_id', 'id')
+ ->addFieldResult('activityType', 'type_name', 'name')
+ ->addJoinedEntityResult(ActivityPresence::class, 'activityPresence', 'a', 'attendee')
+ ->addFieldResult('activityPresence', 'presence_id', 'id')
+ ->addFieldResult('activityPresence', 'presence_name', 'name')
+
+ // results which cannot be mapped into entity
+ ->addScalarResult('comment_comment', 'comment', Types::TEXT)
+ ->addScalarResult('userids', 'userIds', Types::JSON)
+ ->addScalarResult('thirdpartyids', 'thirdPartyIds', Types::JSON)
+ ->addScalarResult('personids', 'personIds', Types::JSON)
+ ->addScalarResult('socialactionids', 'socialActionIds', Types::JSON)
+ ->addScalarResult('socialissueids', 'socialIssueIds', Types::JSON)
+ ->addScalarResult('durationtimeminute', 'durationTimeMinute', Types::INTEGER)
+ ->addScalarResult('traveltimeminute', 'travelTimeMinute', Types::INTEGER);
+
+ $nq = $this->em->createNativeQuery($sql, $rsm);
+
+ $nq->setParameter(0, $period->getId())->setParameter(1, $limit);
+
+ return $nq->getResult(AbstractQuery::HYDRATE_ARRAY);
+ }
+
/**
* @param array $orderBy
*
diff --git a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php
index 56fb112f9..1fc6d22b1 100644
--- a/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php
+++ b/src/Bundle/ChillActivityBundle/Repository/ActivityACLAwareRepositoryInterface.php
@@ -21,6 +21,15 @@ interface ActivityACLAwareRepositoryInterface
*/
public function findByAccompanyingPeriod(AccompanyingPeriod $period, string $role, ?int $start = 0, ?int $limit = 1000, ?array $orderBy = []): array;
+ /**
+ * Return a list of activities, simplified as array (not object).
+ *
+ * The aim of this method is to get a long list of activities and keep performance.
+ *
+ * @return array an array of array, each item representing an activity
+ */
+ public function findByAccompanyingPeriodSimplified(AccompanyingPeriod $period, ?int $limit = 1000): array;
+
/**
* @return Activity[]|array
*/
diff --git a/src/Bundle/ChillActivityBundle/Resources/public/page/edit_activity/index.scss b/src/Bundle/ChillActivityBundle/Resources/public/page/edit_activity/index.scss
index 16e66eadb..5d1ccf976 100644
--- a/src/Bundle/ChillActivityBundle/Resources/public/page/edit_activity/index.scss
+++ b/src/Bundle/ChillActivityBundle/Resources/public/page/edit_activity/index.scss
@@ -4,6 +4,13 @@ div.chill-dropzone__below-zone {
}
}
+ul[data-collection-name="documents"] {
+ button.remove-entry {
+ margin: 0.5rem 0!important;
+ }
+}
+
+
// do it in js does not work
// document.addEventListener('DOMContentLoaded', e => {
// const dropzoneBelow = document.querySelectorAll('div.chill-dropzone__below-zone');
diff --git a/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php
new file mode 100644
index 000000000..0a442cec7
--- /dev/null
+++ b/src/Bundle/ChillActivityBundle/Service/DocGenerator/ListActivitiesByAccompanyingPeriodContext.php
@@ -0,0 +1,286 @@
+accompanyingPeriodContext = $accompanyingPeriodContext;
+ $this->activityACLAwareRepository = $activityACLAwareRepository;
+ $this->normalizer = $normalizer;
+ $this->personRepository = $personRepository;
+ $this->socialActionRepository = $socialActionRepository;
+ $this->socialIssueRepository = $socialIssueRepository;
+ $this->thirdPartyRepository = $thirdPartyRepository;
+ $this->translatableStringHelper = $translatableStringHelper;
+ $this->userRepository = $userRepository;
+ }
+
+ public function adminFormReverseTransform(array $data): array
+ {
+ return $this->accompanyingPeriodContext->adminFormReverseTransform($data);
+ }
+
+ public function adminFormTransform(array $data): array
+ {
+ return $this->accompanyingPeriodContext->adminFormTransform($data);
+ }
+
+ public function buildAdminForm(FormBuilderInterface $builder): void
+ {
+ $this->accompanyingPeriodContext->buildAdminForm($builder);
+ }
+
+ public function buildPublicForm(FormBuilderInterface $builder, DocGeneratorTemplate $template, $entity): void
+ {
+ $this->accompanyingPeriodContext->buildPublicForm($builder, $template, $entity);
+ }
+
+ public function getData(DocGeneratorTemplate $template, $entity, array $contextGenerationData = []): array
+ {
+ $data = $this->accompanyingPeriodContext->getData($template, $entity, $contextGenerationData);
+
+ $data['activities'] = $this->getActivitiesSimplified($entity);
+
+ return $data;
+ }
+
+ public function getDescription(): string
+ {
+ return 'docgen.Accompanying period with a list of activities description';
+ }
+
+ public function getEntityClass(): string
+ {
+ return AccompanyingPeriod::class;
+ }
+
+ public function getFormData(DocGeneratorTemplate $template, $entity): array
+ {
+ return $this->accompanyingPeriodContext->getFormData($template, $entity);
+ }
+
+ public static function getKey(): string
+ {
+ return self::class;
+ }
+
+ public function getName(): string
+ {
+ return 'docgen.Accompanying period with a list of activities';
+ }
+
+ public function hasAdminForm(): bool
+ {
+ return $this->accompanyingPeriodContext->hasAdminForm();
+ }
+
+ public function hasPublicForm(DocGeneratorTemplate $template, $entity): bool
+ {
+ return $this->accompanyingPeriodContext->hasPublicForm($template, $entity);
+ }
+
+ public function storeGenerated(DocGeneratorTemplate $template, StoredObject $storedObject, object $entity, array $contextGenerationData): void
+ {
+ $this->accompanyingPeriodContext->storeGenerated($template, $storedObject, $entity, $contextGenerationData);
+ }
+
+ private function getActivitiesSimplified(AccompanyingPeriod $period)
+ {
+ $activities =
+ $this->activityACLAwareRepository->findByAccompanyingPeriodSimplified($period);
+ $results = [];
+
+ foreach ($activities as $row) {
+ $activity = $row[0];
+
+ $activity['date'] = $this->normalizer->normalize($activity['date'], 'docgen', [
+ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => DateTime::class,
+ ]);
+
+ if (null === $activity['location']) {
+ $activity['location'] = $this->normalizer->normalize(null, 'docgen', [
+ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Location::class,
+ ]);
+ $activity['location']['type'] = 'location';
+ } else {
+ $activity['location']['isNull'] = false;
+ $activity['location']['type'] = 'location';
+
+ foreach (['1', '2'] as $key) {
+ $activity['location']['phonenumber' . $key] = $this->normalizer->normalize(
+ $activity['location']['phonenumber' . $key],
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => PhoneNumber::class]
+ );
+ }
+ }
+
+ if (is_numeric($activity['location']['locationType']['id'])) {
+ $activity['location']['locationType']['title'] = $this->translatableStringHelper->localize(
+ $activity['location']['locationType']['title']
+ );
+ $activity['location']['locationType']['isNull'] = false;
+ $activity['location']['locationType']['type'] = 'locationType';
+ }
+
+ if (null !== $activity['activityType']) {
+ $activity['activityType']['name'] = $this->translatableStringHelper->localize(
+ $activity['activityType']['name']
+ );
+ $activity['activityType']['isNull'] = false;
+ $activity['activityType']['type'] = 'activityType';
+ } else {
+ $activity['activityType'] = $this->normalizer->normalize(null, 'docgen', [
+ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityType::class,
+ ]);
+ }
+
+ if (null !== $activity['attendee']) {
+ $activity['attendee']['name'] = $this->translatableStringHelper->localize(
+ $activity['attendee']['name']
+ );
+ $activity['attendee']['isNull'] = false;
+ $activity['attendee']['type'] = 'activityPresence';
+ } else {
+ $activity['attendee'] = $this->normalizer->normalize(null, 'docgen', [
+ AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ActivityPresence::class,
+ ]);
+ }
+
+ $activity['comment'] = (string) $row['comment'];
+ $activity['travelTimeMinute'] = $row['travelTimeMinute'];
+ $activity['durationTimeMinute'] = $row['durationTimeMinute'];
+
+ if (null !== $row['userIds']) {
+ foreach ($row['userIds'] as $id) {
+ $activity['users'][] = $this->normalizer->normalize(
+ $this->userRepository->find($id),
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => User::class]
+ );
+ }
+ } else {
+ $activity['users'] = [];
+ }
+
+ if (null !== $row['personIds']) {
+ foreach ($row['personIds'] as $id) {
+ $activity['persons'][] = $this->normalizer->normalize(
+ $this->personRepository->find($id),
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => Person::class]
+ );
+ }
+ } else {
+ $activity['persons'] = [];
+ }
+
+ if (null !== $row['thirdPartyIds']) {
+ foreach ($row['thirdPartyIds'] as $id) {
+ $activity['thirdParties'][] = $this->normalizer->normalize(
+ $this->thirdPartyRepository->find($id),
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => ThirdParty::class]
+ );
+ }
+ } else {
+ $activity['thirdParties'] = [];
+ }
+
+ if (null !== $row['socialActionIds']) {
+ foreach ($row['socialActionIds'] as $id) {
+ $activity['socialActions'][] = $this->normalizer->normalize(
+ $this->socialActionRepository->find($id),
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialAction::class]
+ );
+ }
+ } else {
+ $activity['socialActions'] = [];
+ }
+
+ if (null !== $row['socialIssueIds']) {
+ foreach ($row['socialIssueIds'] as $id) {
+ $activity['socialIssues'][] = $this->normalizer->normalize(
+ $this->socialIssueRepository->find($id),
+ 'docgen',
+ [AbstractNormalizer::GROUPS => ['docgen:read'], 'docgen:expects' => SocialIssue::class]
+ );
+ }
+ } else {
+ $activity['socialIssues'] = [];
+ }
+
+ $results[] = $activity;
+ }
+
+ return $results;
+ }
+}
diff --git a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
index 051497b69..49217dd5d 100644
--- a/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
+++ b/src/Bundle/ChillActivityBundle/translations/messages.fr.yml
@@ -232,3 +232,5 @@ This is the minimal activity data: Activité n°
docgen:
Activity basic: Echange
A basic context for activity: Contexte pour les échanges
+ Accompanying period with a list of activities: Parcours d'accompagnement avec liste des échanges
+ Accompanying period with a list of activities description: Ce contexte reprend les informations du parcours, et tous les échanges pour un parcours. Les échanges ne sont pas filtrés.
diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php
index 08a0fec77..243cd4749 100644
--- a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php
+++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudget.php
@@ -23,15 +23,15 @@ use function count;
/**
* Helps to find a summary of the budget: the sum of resources and charges.
*/
-class SummaryBudget
+class SummaryBudget implements SummaryBudgetInterface
{
- private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
+ private const QUERY_CHARGE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
- private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
+ private const QUERY_CHARGE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.charge WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
- private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
+ private const QUERY_RESOURCE_BY_HOUSEHOLD = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE (person_id IN (_ids_) OR household_id = ?) AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
- private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
+ private const QUERY_RESOURCE_BY_PERSON = 'select SUM(amount) AS sum, string_agg(comment, \'|\') AS comment, type FROM chill_budget.resource WHERE person_id = ? AND NOW() BETWEEN startdate AND COALESCE(enddate, \'infinity\'::timestamp) GROUP BY type';
private array $chargeLabels;
@@ -52,26 +52,6 @@ class SummaryBudget
$this->translatableStringHelper = $translatableStringHelper;
}
- public function getEmptyChargeArray(): array
- {
- $keys = $this->configRepository->getChargesKeys();
- $labels = $this->chargeLabels;
-
- return array_combine($keys, array_map(function ($i) use ($labels) {
- return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
- }, $keys));
- }
-
- public function getEmptyResourceArray(): array
- {
- $keys = $this->configRepository->getResourcesKeys();
- $labels = $this->resourcesLabels;
-
- return array_combine($keys, array_map(function ($i) use ($labels) {
- return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i])];
- }, $keys));
- }
-
public function getSummaryForHousehold(?Household $household): array
{
if (null === $household) {
@@ -101,8 +81,15 @@ class SummaryBudget
];
}
- public function getSummaryForPerson(Person $person): array
+ public function getSummaryForPerson(?Person $person): array
{
+ if (null === $person) {
+ return [
+ 'resources' => $this->getEmptyResourceArray(),
+ 'charges' => $this->getEmptyChargeArray(),
+ ];
+ }
+
$rsm = $this->buildRsm();
$resources = $this->em->createNativeQuery(self::QUERY_RESOURCE_BY_PERSON, $rsm)
@@ -123,11 +110,32 @@ class SummaryBudget
$rsm = new ResultSetMapping();
$rsm
->addScalarResult('sum', 'sum')
- ->addScalarResult('type', 'type');
+ ->addScalarResult('type', 'type')
+ ->addScalarResult('comment', 'comment');
return $rsm;
}
+ private function getEmptyChargeArray(): array
+ {
+ $keys = $this->configRepository->getChargesKeys();
+ $labels = $this->chargeLabels;
+
+ return array_combine($keys, array_map(function ($i) use ($labels) {
+ return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
+ }, $keys));
+ }
+
+ private function getEmptyResourceArray(): array
+ {
+ $keys = $this->configRepository->getResourcesKeys();
+ $labels = $this->resourcesLabels;
+
+ return array_combine($keys, array_map(function ($i) use ($labels) {
+ return ['sum' => 0.0, 'label' => $this->translatableStringHelper->localize($labels[$i]), 'comment' => ''];
+ }, $keys));
+ }
+
private function rowToArray(array $rows, string $kind): array
{
switch ($kind) {
@@ -151,6 +159,7 @@ class SummaryBudget
$result[$row['type']] = [
'sum' => (float) $row['sum'],
'label' => $this->translatableStringHelper->localize($label[$row['type']]),
+ 'comment' => (string) $row['comment'],
];
}
diff --git a/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudgetInterface.php b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudgetInterface.php
new file mode 100644
index 000000000..528c4626e
--- /dev/null
+++ b/src/Bundle/ChillBudgetBundle/Service/Summary/SummaryBudgetInterface.php
@@ -0,0 +1,25 @@
+discovery->discoverMimeType($document->getType());
-
- foreach ($mime_type as $item) {
- if (array_key_exists('default', $item) && 'true' === $item['default']) {
- return true;
- }
- }
-
- return false;
+ return in_array($document->getType(), self::SUPPORTED_MIMES, true);
}
public function renderEditButton(Environment $environment, StoredObject $document, ?array $options = null): string
diff --git a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
index 3d7b95c61..02cdf6fb3 100644
--- a/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
+++ b/src/Bundle/ChillDocStoreBundle/Workflow/AccompanyingCourseDocumentWorkflowHandler.php
@@ -116,6 +116,6 @@ class AccompanyingCourseDocumentWorkflowHandler implements EntityWorkflowHandler
public function supportsFreeze(EntityWorkflow $entityWorkflow, array $options = []): bool
{
- return true;
+ return false;
}
}
diff --git a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
index cfbe811fa..a780af78a 100644
--- a/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
+++ b/src/Bundle/ChillMainBundle/CRUD/Controller/ApiController.php
@@ -33,11 +33,11 @@ class ApiController extends AbstractCRUDController
* Base method for handling api action.
*
* @param mixed $id
- * @param mixed $_format
+ * @param string $_format
*
* @return void
*/
- public function entityApi(Request $request, $id, $_format): Response
+ public function entityApi(Request $request, $id, ?string $_format = 'json'): Response
{
switch ($request->getMethod()) {
case Request::METHOD_GET:
diff --git a/src/Bundle/ChillMainBundle/Controller/LocationController.php b/src/Bundle/ChillMainBundle/Controller/LocationController.php
index 10087ec1e..97deac9c5 100644
--- a/src/Bundle/ChillMainBundle/Controller/LocationController.php
+++ b/src/Bundle/ChillMainBundle/Controller/LocationController.php
@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Controller;
use Chill\MainBundle\CRUD\Controller\CRUDController;
+use Chill\MainBundle\Pagination\PaginatorInterface;
use Symfony\Component\HttpFoundation\Request;
class LocationController extends CRUDController
@@ -29,4 +30,9 @@ class LocationController extends CRUDController
{
$query->where('e.availableForUsers = true'); //TODO not working
}
+
+ protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator)
+ {
+ return $query->addOrderBy('e.name', 'DESC');
+ }
}
diff --git a/src/Bundle/ChillMainBundle/Controller/NotificationController.php b/src/Bundle/ChillMainBundle/Controller/NotificationController.php
index af9e1b2b1..f40c85ffb 100644
--- a/src/Bundle/ChillMainBundle/Controller/NotificationController.php
+++ b/src/Bundle/ChillMainBundle/Controller/NotificationController.php
@@ -22,6 +22,7 @@ use Chill\MainBundle\Pagination\PaginatorFactory;
use Chill\MainBundle\Repository\NotificationRepository;
use Chill\MainBundle\Security\Authorization\NotificationVoter;
use Doctrine\ORM\EntityManagerInterface;
+use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
@@ -31,14 +32,19 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Contracts\Translation\TranslatorInterface;
+use function in_array;
/**
* @Route("/{_locale}/notification")
*/
class NotificationController extends AbstractController
{
+ private LoggerInterface $chillLogger;
+
private EntityManagerInterface $em;
+ private LoggerInterface $logger;
+
private NotificationHandlerManager $notificationHandlerManager;
private NotificationRepository $notificationRepository;
@@ -51,6 +57,8 @@ class NotificationController extends AbstractController
public function __construct(
EntityManagerInterface $em,
+ LoggerInterface $chillLogger,
+ LoggerInterface $logger,
Security $security,
NotificationRepository $notificationRepository,
NotificationHandlerManager $notificationHandlerManager,
@@ -58,6 +66,8 @@ class NotificationController extends AbstractController
TranslatorInterface $translator
) {
$this->em = $em;
+ $this->logger = $logger;
+ $this->chillLogger = $chillLogger;
$this->security = $security;
$this->notificationRepository = $notificationRepository;
$this->notificationHandlerManager = $notificationHandlerManager;
@@ -150,6 +160,49 @@ class NotificationController extends AbstractController
]);
}
+ /**
+ * @Route("/{id}/access_key", name="chill_main_notification_grant_access_by_access_key")
+ */
+ public function getAccessByAccessKey(Notification $notification, Request $request): Response
+ {
+ $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
+
+ if (!$this->security->getUser() instanceof User) {
+ throw new AccessDeniedHttpException('You must be authenticated and a user to create a notification');
+ }
+
+ foreach (['accessKey', 'email'] as $param) {
+ if (!$request->query->has($param)) {
+ throw new BadRequestHttpException("Missing {$param} parameter");
+ }
+ }
+
+ if ($notification->getAccessKey() !== $request->query->getAlnum('accessKey')) {
+ throw new AccessDeniedHttpException('access key is invalid');
+ }
+
+ if (!in_array($request->query->get('email'), $notification->getAddressesEmails(), true)) {
+ return (new Response('The email address is no more associated with this notification'))
+ ->setStatusCode(Response::HTTP_FORBIDDEN);
+ }
+
+ $notification->addAddressee($this->security->getUser());
+
+ $this->getDoctrine()->getManager()->flush();
+
+ $logMsg = '[Notification] a user is granted access to notification trough an access key';
+ $context = [
+ 'notificationId' => $notification->getId(),
+ 'email' => $request->query->get('email'),
+ 'user' => $this->security->getUser()->getId(),
+ ];
+
+ $this->logger->info($logMsg, $context);
+ $this->chillLogger->info($logMsg, $context);
+
+ return $this->redirectToRoute('chill_main_notification_show', ['id' => $notification->getId()]);
+ }
+
/**
* @Route("/inbox", name="chill_main_notification_my")
*/
diff --git a/src/Bundle/ChillMainBundle/Controller/SearchController.php b/src/Bundle/ChillMainBundle/Controller/SearchController.php
index b9f5d19d8..41349336a 100644
--- a/src/Bundle/ChillMainBundle/Controller/SearchController.php
+++ b/src/Bundle/ChillMainBundle/Controller/SearchController.php
@@ -122,7 +122,7 @@ class SearchController extends AbstractController
public function searchAction(Request $request, $_format)
{
- $pattern = $request->query->get('q', '');
+ $pattern = trim($request->query->get('q', ''));
if ('' === $pattern) {
switch ($_format) {
diff --git a/src/Bundle/ChillMainBundle/Entity/Notification.php b/src/Bundle/ChillMainBundle/Entity/Notification.php
index b2fe40c60..8dad39d12 100644
--- a/src/Bundle/ChillMainBundle/Entity/Notification.php
+++ b/src/Bundle/ChillMainBundle/Entity/Notification.php
@@ -18,6 +18,9 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
+use Symfony\Component\Validator\Context\ExecutionContextInterface;
+use function count;
+use function in_array;
/**
* @ORM\Entity
@@ -31,15 +34,34 @@ use Symfony\Component\Validator\Constraints as Assert;
*/
class Notification implements TrackUpdateInterface
{
+ /**
+ * @ORM\Column(type="text", nullable=false)
+ */
+ private string $accessKey;
+
private array $addedAddresses = [];
/**
* @ORM\ManyToMany(targetEntity=User::class)
* @ORM\JoinTable(name="chill_main_notification_addresses_user")
- * @Assert\Count(min="1", minMessage="notification.At least one addressee")
*/
private Collection $addressees;
+ /**
+ * a list of destinee which will receive notifications.
+ *
+ * @var array|string[]
+ * @ORM\Column(type="json")
+ */
+ private array $addressesEmails = [];
+
+ /**
+ * a list of emails adresses which were added to the notification.
+ *
+ * @var array|string[]
+ */
+ private array $addressesEmailsAdded = [];
+
private ?ArrayCollection $addressesOnLoad = null;
/**
@@ -111,6 +133,7 @@ class Notification implements TrackUpdateInterface
$this->unreadBy = new ArrayCollection();
$this->comments = new ArrayCollection();
$this->setDate(new DateTimeImmutable());
+ $this->accessKey = bin2hex(openssl_random_pseudo_bytes(24));
}
public function addAddressee(User $addressee): self
@@ -123,6 +146,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
+ public function addAddressesEmail(string $email)
+ {
+ if (!in_array($email, $this->addressesEmails, true)) {
+ $this->addressesEmails[] = $email;
+ $this->addressesEmailsAdded[] = $email;
+ }
+ }
+
public function addComment(NotificationComment $comment): self
{
if (!$this->comments->contains($comment)) {
@@ -142,6 +173,30 @@ class Notification implements TrackUpdateInterface
return $this;
}
+ /**
+ * @Assert\Callback
+ *
+ * @param array $payload
+ */
+ public function assertCountAddresses(ExecutionContextInterface $context, $payload): void
+ {
+ if (0 === (count($this->getAddressesEmails()) + count($this->getAddressees()))) {
+ $context->buildViolation('notification.At least one addressee')
+ ->atPath('addressees')
+ ->addViolation();
+ }
+ }
+
+ public function getAccessKey(): string
+ {
+ return $this->accessKey;
+ }
+
+ public function getAddedAddresses(): array
+ {
+ return $this->addedAddresses;
+ }
+
/**
* @return Collection|User[]
*/
@@ -155,6 +210,22 @@ class Notification implements TrackUpdateInterface
return $this->addressees;
}
+ /**
+ * @return array|string[]
+ */
+ public function getAddressesEmails(): array
+ {
+ return $this->addressesEmails;
+ }
+
+ /**
+ * @return array|string[]
+ */
+ public function getAddressesEmailsAdded(): array
+ {
+ return $this->addressesEmailsAdded;
+ }
+
public function getComments(): Collection
{
return $this->comments;
@@ -271,6 +342,14 @@ class Notification implements TrackUpdateInterface
return $this;
}
+ public function removeAddressesEmail(string $email)
+ {
+ if (in_array($email, $this->addressesEmails, true)) {
+ $this->addressesEmails = array_filter($this->addressesEmails, static fn ($e) => $e !== $email);
+ $this->addressesEmailsAdded = array_filter($this->addressesEmailsAdded, static fn ($e) => $e !== $email);
+ }
+ }
+
public function removeComment(NotificationComment $comment): self
{
$this->comments->removeElement($comment);
diff --git a/src/Bundle/ChillMainBundle/Form/NotificationType.php b/src/Bundle/ChillMainBundle/Form/NotificationType.php
index b24513524..22fd19baf 100644
--- a/src/Bundle/ChillMainBundle/Form/NotificationType.php
+++ b/src/Bundle/ChillMainBundle/Form/NotificationType.php
@@ -12,12 +12,17 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\Notification;
+use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickUserDynamicType;
use Symfony\Component\Form\AbstractType;
+use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
+use Symfony\Component\Validator\Constraints\Email;
+use Symfony\Component\Validator\Constraints\NotBlank;
+use Symfony\Component\Validator\Constraints\NotNull;
class NotificationType extends AbstractType
{
@@ -30,9 +35,27 @@ class NotificationType extends AbstractType
])
->add('addressees', PickUserDynamicType::class, [
'multiple' => true,
+ 'required' => false,
])
->add('message', ChillTextareaType::class, [
'required' => false,
+ ])
+ ->add('addressesEmails', ChillCollectionType::class, [
+ 'label' => 'notification.dest by email',
+ 'help' => 'notification.dest by email help',
+ 'by_reference' => false,
+ 'allow_add' => true,
+ 'allow_delete' => true,
+ 'entry_type' => EmailType::class,
+ 'button_add_label' => 'notification.Add an email',
+ 'button_remove_label' => 'notification.Remove an email',
+ 'empty_collection_explain' => 'notification.Any email',
+ 'entry_options' => [
+ 'constraints' => [
+ new NotNull(), new NotBlank(), new Email(['checkMX' => true]),
+ ],
+ 'label' => 'Email',
+ ],
]);
}
diff --git a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php
index ce5587ed9..69fcf66f0 100644
--- a/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php
+++ b/src/Bundle/ChillMainBundle/Notification/Email/NotificationMailer.php
@@ -73,6 +73,17 @@ class NotificationMailer
* Send a email after a notification is persisted.
*/
public function postPersistNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
+ {
+ $this->sendNotificationEmailsToAddresses($notification);
+ $this->sendNotificationEmailsToAddressesEmails($notification);
+ }
+
+ public function postUpdateNotification(Notification $notification, LifecycleEventArgs $eventArgs): void
+ {
+ $this->sendNotificationEmailsToAddressesEmails($notification);
+ }
+
+ private function sendNotificationEmailsToAddresses(Notification $notification): void
{
foreach ($notification->getAddressees() as $addressee) {
if (null === $addressee->getEmail()) {
@@ -108,4 +119,31 @@ class NotificationMailer
}
}
}
+
+ private function sendNotificationEmailsToAddressesEmails(Notification $notification): void
+ {
+ foreach ($notification->getAddressesEmailsAdded() as $emailAddress) {
+ $email = new TemplatedEmail();
+ $email
+ ->textTemplate('@ChillMain/Notification/email_non_system_notification_content_to_email.fr.md.twig')
+ ->context([
+ 'notification' => $notification,
+ 'dest' => $emailAddress,
+ ]);
+
+ $email
+ ->subject($notification->getTitle())
+ ->to($emailAddress);
+
+ try {
+ $this->mailer->send($email);
+ } catch (TransportExceptionInterface $e) {
+ $this->logger->warning('[NotificationMailer] could not send an email notification', [
+ 'to' => $emailAddress,
+ 'error_message' => $e->getMessage(),
+ 'error_trace' => $e->getTraceAsString(),
+ ]);
+ }
+ }
+ }
}
diff --git a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
index 9cfffb2cf..3f3b08f6c 100644
--- a/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
+++ b/src/Bundle/ChillMainBundle/Repository/NotificationRepository.php
@@ -56,17 +56,18 @@ final class NotificationRepository implements ObjectRepository
if (null === $this->notificationByRelatedEntityAndUserAssociatedStatement) {
$sql =
'SELECT
- SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau WHERE user_id = 1812 and cmnau.notification_id = cmn.id))::int) AS unread,
- SUM((cmn.sender_id = 1812)::int) AS sent,
+ SUM((EXISTS (SELECT 1 AS c FROM chill_main_notification_addresses_unread cmnau JOIN chill_main_notification cmn ON cmnau.notification_id = cmn.id WHERE user_id = :userid and cmnau.notification_id = cmn.id and cmn.sender_id IS NOT NULL))::int) AS unread,
+ SUM((cmn.sender_id = :userid)::int) AS sent,
COUNT(cmn.*) AS total
FROM chill_main_notification cmn
WHERE relatedentityclass = :relatedEntityClass AND relatedentityid = :relatedEntityId AND sender_id IS NOT NULL';
+
$this->notificationByRelatedEntityAndUserAssociatedStatement =
$this->em->getConnection()->prepare($sql);
}
$results = $this->notificationByRelatedEntityAndUserAssociatedStatement
- ->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId]);
+ ->executeQuery(['relatedEntityClass' => $relatedEntityClass, 'relatedEntityId' => $relatedEntityId, 'userid' => $user->getId()]);
$result = $results->fetchAssociative();
diff --git a/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js
index 68b06b76a..ae092f8e2 100644
--- a/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js
+++ b/src/Bundle/ChillMainBundle/Resources/public/module/notification/toggle_read.js
@@ -8,30 +8,31 @@ window.addEventListener('DOMContentLoaded', function (e) {
document.querySelectorAll('.notification_toggle_read_status')
.forEach(function (el, i) {
createApp({
- template: '
{{ 'Any comment'|trans }}
+ {% endif %} {% else %} - {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} - + {% if c.notification.message is not empty %} + {{ c.notification.message|u.truncate(250, '…', false)|chill_markdown_to_html }} + + {% else %} +{{ 'Any comment'|trans }}
+ {% endif %} {% endif %}{{ 'accompanying_course_work.Any work'|trans }} - -
- +{{ 'accompanying_course_work.Any work'|trans }}
{% else %} {% endblock %} diff --git a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig index 65e514b14..62d0682df 100644 --- a/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig +++ b/src/Bundle/ChillPersonBundle/Resources/views/AccompanyingPeriod/_list_item.html.twig @@ -116,14 +116,40 @@